diff --git a/src/OneGround.ZGW.Autorisaties.ServiceAgent/AutorisatiesServiceAgent.cs b/src/OneGround.ZGW.Autorisaties.ServiceAgent/AutorisatiesServiceAgent.cs index 1b933d66..94a7b818 100644 --- a/src/OneGround.ZGW.Autorisaties.ServiceAgent/AutorisatiesServiceAgent.cs +++ b/src/OneGround.ZGW.Autorisaties.ServiceAgent/AutorisatiesServiceAgent.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -using OneGround.ZGW.Autorisaties.Contracts.v1.Responses; +using OneGround.ZGW.Autorisaties.Contracts.v1._1.Responses; using OneGround.ZGW.Common.Constants; using OneGround.ZGW.Common.Extensions; using OneGround.ZGW.Common.ServiceAgent; diff --git a/src/OneGround.ZGW.Autorisaties.ServiceAgent/AutorisatiesServiceAgentAuthorizationResolver.cs b/src/OneGround.ZGW.Autorisaties.ServiceAgent/AutorisatiesServiceAgentAuthorizationResolver.cs index 2a5f20d5..2f96143b 100644 --- a/src/OneGround.ZGW.Autorisaties.ServiceAgent/AutorisatiesServiceAgentAuthorizationResolver.cs +++ b/src/OneGround.ZGW.Autorisaties.ServiceAgent/AutorisatiesServiceAgentAuthorizationResolver.cs @@ -27,6 +27,7 @@ public async Task ResolveAsync(string clientId, string co return new AuthorizedApplication { Label = application.Label, + AlleenIsGereedVoorPublicatie = application.AlleenIsGereedVoorPublicatie, HasAllAuthorizations = application.HeeftAlleAutorisaties, Authorizations = application .Autorisaties.Where(a => a.Component.Equals(component, StringComparison.OrdinalIgnoreCase)) diff --git a/src/OneGround.ZGW.Autorisaties.ServiceAgent/IAutorisatiesServiceAgent.cs b/src/OneGround.ZGW.Autorisaties.ServiceAgent/IAutorisatiesServiceAgent.cs index c975a0d4..4cf09f12 100644 --- a/src/OneGround.ZGW.Autorisaties.ServiceAgent/IAutorisatiesServiceAgent.cs +++ b/src/OneGround.ZGW.Autorisaties.ServiceAgent/IAutorisatiesServiceAgent.cs @@ -1,6 +1,6 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using OneGround.ZGW.Autorisaties.Contracts.v1.Responses; +using OneGround.ZGW.Autorisaties.Contracts.v1._1.Responses; using OneGround.ZGW.Common.ServiceAgent; namespace OneGround.ZGW.Autorisaties.ServiceAgent; diff --git a/src/OneGround.ZGW.Autorisaties.Web/Services/DbContextAuthorizationResolver.cs b/src/OneGround.ZGW.Autorisaties.Web/Services/DbContextAuthorizationResolver.cs index 0d5d9dd6..f4b234be 100644 --- a/src/OneGround.ZGW.Autorisaties.Web/Services/DbContextAuthorizationResolver.cs +++ b/src/OneGround.ZGW.Autorisaties.Web/Services/DbContextAuthorizationResolver.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -31,6 +31,7 @@ public async Task ResolveAsync(string clientId, string co { Label = applicatie.Label, HasAllAuthorizations = applicatie.HeeftAlleAutorisaties, + AlleenIsGereedVoorPublicatie = applicatie.AlleenIsGereedVoorPublicatie, Authorizations = applicatie .Autorisaties.Where(a => a.Component.ToString().Equals(component, StringComparison.OrdinalIgnoreCase)) .Where(a => a.Scopes.Any(s => scopes.Contains(s))) diff --git a/src/OneGround.ZGW.Common.Web/Authorization/AuthorizedApplication.cs b/src/OneGround.ZGW.Common.Web/Authorization/AuthorizedApplication.cs index 62118689..260a0a00 100644 --- a/src/OneGround.ZGW.Common.Web/Authorization/AuthorizedApplication.cs +++ b/src/OneGround.ZGW.Common.Web/Authorization/AuthorizedApplication.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace OneGround.ZGW.Common.Web.Authorization; @@ -23,4 +23,9 @@ public class AuthorizedApplication /// Gets rsin value which application is authorized for. /// public string Rsin { get; set; } + + /// + /// Flag indicating 'Only Ready for publication'. + /// + public bool AlleenIsGereedVoorPublicatie { get; set; } } diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/EnkelvoudigInformatieObjectDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/EnkelvoudigInformatieObjectDto.cs new file mode 100644 index 00000000..5ba0eade --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/EnkelvoudigInformatieObjectDto.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace OneGround.ZGW.Documenten.Contracts.v1._7; + +public class EnkelvoudigInformatieObjectDto +{ + [JsonProperty(PropertyName = "identificatie")] + public string Identificatie { get; set; } + + [JsonProperty(PropertyName = "bronorganisatie")] + public string Bronorganisatie { get; set; } + + [JsonProperty(PropertyName = "creatiedatum")] + public string CreatieDatum { get; set; } + + [JsonProperty(PropertyName = "titel")] + public string Titel { get; set; } + + [JsonProperty(PropertyName = "vertrouwelijkheidaanduiding")] + public string Vertrouwelijkheidaanduiding { get; set; } + + [JsonProperty(PropertyName = "isGereedVoorPublicatie")] + public bool? IsGereedVoorPublicatie { get; set; } + + [JsonProperty(PropertyName = "tonenAanInitiator")] + public bool TonenAanInitiator { get; set; } + + [JsonProperty(PropertyName = "auteur")] + public string Auteur { get; set; } + + [JsonProperty(PropertyName = "status")] + public string Status { get; set; } + + [JsonProperty(PropertyName = "formaat")] + public string Formaat { get; set; } = string.Empty; // Note: null is not allowed. When not specified it will use empty string now + + [JsonProperty(PropertyName = "taal")] + public string Taal { get; set; } + + [JsonProperty(PropertyName = "bestandsnaam")] + public string Bestandsnaam { get; set; } + + [JsonProperty(PropertyName = "bestandsomvang")] + public long Bestandsomvang { get; set; } + + [JsonProperty(PropertyName = "inhoud")] + public string Inhoud { get; set; } + + [JsonProperty(PropertyName = "link")] + public string Link { get; set; } + + [JsonProperty(PropertyName = "beschrijving")] + public string Beschrijving { get; set; } + + [JsonProperty(PropertyName = "ontvangstdatum")] + public string OntvangstDatum { get; set; } + + [JsonProperty(PropertyName = "verzenddatum")] + public string VerzendDatum { get; set; } + + [JsonProperty(PropertyName = "indicatieGebruiksrecht")] + public bool? IndicatieGebruiksrecht { get; set; } + + [JsonProperty(PropertyName = "ondertekening")] + public OndertekeningDto Ondertekening { get; set; } + + [JsonProperty(PropertyName = "integriteit")] + public IntegriteitDto Integriteit { get; set; } + + [JsonProperty(PropertyName = "informatieobjecttype")] + public string InformatieObjectType { get; set; } + + [JsonProperty(PropertyName = "verschijningsvorm")] + public string Verschijningsvorm { get; set; } + + [JsonProperty(PropertyName = "inhoudIsVervallen")] + public bool InhoudIsVervallen { get; set; } + + [JsonProperty("trefwoorden")] + public List Trefwoorden { get; set; } = []; +} diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/IDocumentenCommonSearchableFields.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/IDocumentenCommonSearchableFields.cs new file mode 100644 index 00000000..3cf623dc --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/IDocumentenCommonSearchableFields.cs @@ -0,0 +1,29 @@ +namespace OneGround.ZGW.Documenten.Contracts.v1._7; + +public interface IDocumentenCommonSearchableFields +{ + /// + /// Het RSIN van de Niet-natuurlijk persoon zijnde de organisatie die het informatieobject heeft gecreëerd of heeft ontvangen en als eerste in een samenwerkingsketen heeft vastgelegd. + /// + public string Bronorganisatie { get; set; } + + /// + /// Een binnen een gegeven context ondubbelzinnige referentie naar het INFORMATIEOBJECT. + /// + public string Identificatie { get; set; } + + /// + /// Een lijst van trefwoorden gescheiden door comma's. Example: trefwoorden=bouwtekening,vergunning,aanvraag + /// + public string Trefwoorden { get; set; } + + /// + /// URL-referentie naar het gerelateerde OBJECT (in deze of andere API). + /// + public string ObjectInformatieObjecten_Object { get; set; } + + /// + /// Enum: "besluit" "zaak" "verzoek" - Het type van het gerelateerde OBJECT. + /// + public string ObjectInformatieObjecten_ObjectType { get; set; } +} diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Queries/GetAllEnkelvoudiginformatieobjectenQueryParameters.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Queries/GetAllEnkelvoudiginformatieobjectenQueryParameters.cs new file mode 100644 index 00000000..1e3f0f1e --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Queries/GetAllEnkelvoudiginformatieobjectenQueryParameters.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc; +using OneGround.ZGW.Common.Contracts; + +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Queries; + +public class GetAllEnkelvoudigInformatieObjectenQueryParameters : QueryParameters, IDocumentenCommonSearchableFields, IExpandParameter +{ + /// + /// Het RSIN van de Niet-natuurlijk persoon zijnde de organisatie die het informatieobject heeft gecreëerd of heeft ontvangen en als eerste in een samenwerkingsketen heeft vastgelegd. + /// + [FromQuery(Name = "bronorganisatie")] + public string Bronorganisatie { get; set; } + + /// + /// Een binnen een gegeven context ondubbelzinnige referentie naar het INFORMATIEOBJECT. + /// + [FromQuery(Name = "identificatie")] + public string Identificatie { get; set; } + + /// + /// Een lijst van trefwoorden gescheiden door comma's. Example: trefwoorden=bouwtekening,vergunning,aanvraag + /// + [FromQuery(Name = "trefwoorden")] + public string Trefwoorden { get; set; } + + /// + /// URL-referentie naar het gerelateerde OBJECT (in deze of andere API). + /// + [FromQuery(Name = "objectinformatieobjecten_object")] + public string ObjectInformatieObjecten_Object { get; set; } + + /// + /// Enum: "besluit" "zaak" "verzoek" - Het type van het gerelateerde OBJECT. + /// + [FromQuery(Name = "objectinformatieobjecten_objectType")] + public string ObjectInformatieObjecten_ObjectType { get; set; } + + /// + /// Expand het respons met sub-types. + /// + [FromQuery(Name = "expand")] + public string Expand { get; set; } +} diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectBaseRequestDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectBaseRequestDto.cs new file mode 100644 index 00000000..548ffe22 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectBaseRequestDto.cs @@ -0,0 +1,3 @@ +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Requests; + +public class EnkelvoudigInformatieObjectBaseRequestDto : EnkelvoudigInformatieObjectDto { } diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectCreateRequestDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectCreateRequestDto.cs new file mode 100644 index 00000000..a88fd33e --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectCreateRequestDto.cs @@ -0,0 +1,3 @@ +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Requests; + +public class EnkelvoudigInformatieObjectCreateRequestDto : EnkelvoudigInformatieObjectBaseRequestDto { } diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectSearchRequestDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectSearchRequestDto.cs new file mode 100644 index 00000000..16fd070b --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectSearchRequestDto.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using OneGround.ZGW.Common.Contracts; + +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Requests; + +public class EnkelvoudigInformatieObjectSearchRequestDto : IDocumentenCommonSearchableFields, IExpandParameter +{ + [JsonProperty("bronorganisatie")] + public string Bronorganisatie { get; set; } + + [JsonProperty("identificatie")] + public string Identificatie { get; set; } + + [JsonProperty("trefwoorden")] + public string Trefwoorden { get; set; } + + [JsonProperty("objectinformatieobjecten_object")] + public string ObjectInformatieObjecten_Object { get; set; } + + [JsonProperty("objectinformatieobjecten_objectType")] + public string ObjectInformatieObjecten_ObjectType { get; set; } + + [JsonProperty("uuid_In")] + public string[] Uuid_In { get; set; } + + [JsonProperty("expand")] + public string Expand { get; set; } +} diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectUpdateRequestDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectUpdateRequestDto.cs new file mode 100644 index 00000000..e5aad960 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Requests/EnkelvoudigInformatieObjectUpdateRequestDto.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Requests; + +public class EnkelvoudigInformatieObjectUpdateRequestDto : EnkelvoudigInformatieObjectBaseRequestDto +{ + [JsonProperty(PropertyName = "lock")] + public string Lock { get; set; } +} diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectCreateResponseDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectCreateResponseDto.cs new file mode 100644 index 00000000..98b174ed --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectCreateResponseDto.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Responses; + +public class EnkelvoudigInformatieObjectCreateResponseDto : EnkelvoudigInformatieObjectResponseDto +{ + [JsonProperty(PropertyName = "lock", Order = 99)] + public string Lock { get; set; } +} diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectGetResponseDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectGetResponseDto.cs new file mode 100644 index 00000000..f83f35ef --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectGetResponseDto.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; +using OneGround.ZGW.Common.Contracts.v1._5; + +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Responses; + +public class EnkelvoudigInformatieObjectGetResponseDto : EnkelvoudigInformatieObjectResponseDto { } + +public class EnkelvoudigInformatieObjectGetResponseExpandedDto : EnkelvoudigInformatieObjectGetResponseDto, IExpandResponseDto +{ + [JsonProperty(PropertyName = "_expand")] + public object Expand { get; set; } +} diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectResponseDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectResponseDto.cs new file mode 100644 index 00000000..56880b75 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectResponseDto.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Responses; + +public class EnkelvoudigInformatieObjectResponseDto : EnkelvoudigInformatieObjectDto +{ + [JsonProperty(PropertyName = "url", Order = -1)] + public string Url { get; set; } + + [JsonProperty(PropertyName = "versie", Order = 70)] + public int Versie { get; set; } + + [JsonProperty(PropertyName = "beginRegistratie", Order = 71)] + public string BeginRegistratie { get; set; } + + [JsonProperty(PropertyName = "locked", Order = 72)] + public bool Locked { get; set; } + + [JsonProperty("bestandsdelen", Order = 73)] + public List<_1.Responses.BestandsDeelResponseDto> BestandsDelen { get; set; } = []; +} diff --git a/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectUpdateResponseDto.cs b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectUpdateResponseDto.cs new file mode 100644 index 00000000..f5d660fb --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Contracts/v1/7/Responses/EnkelvoudigInformatieObjectUpdateResponseDto.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace OneGround.ZGW.Documenten.Contracts.v1._7.Responses; + +public class EnkelvoudigInformatieObjectUpdateResponseDto : EnkelvoudigInformatieObjectResponseDto +{ + [JsonProperty(PropertyName = "lock", Order = 99)] + public string Lock { get; set; } +} diff --git a/src/OneGround.ZGW.Documenten.DataModel/EnkelvoudigInformatieObjectVersie.cs b/src/OneGround.ZGW.Documenten.DataModel/EnkelvoudigInformatieObjectVersie.cs index 693eda35..d21d37b0 100644 --- a/src/OneGround.ZGW.Documenten.DataModel/EnkelvoudigInformatieObjectVersie.cs +++ b/src/OneGround.ZGW.Documenten.DataModel/EnkelvoudigInformatieObjectVersie.cs @@ -128,6 +128,12 @@ public class EnkelvoudigInformatieObjectVersie : OwnedEntity, IAuditableEntity, [Column("inhoud_is_vervallen")] public bool InhoudIsVervallen { get; set; } + [Column("is_gereed_voor_publicatie")] + public bool? IsGereedVoorPublicatie { get; set; } + + [Column("tonen_aan_initiator")] + public bool TonenAanInitiator { get; set; } + [Column("enkelvoudiginformatieobject_id")] public Guid EnkelvoudigInformatieObjectId { get; set; } diff --git a/src/OneGround.ZGW.Documenten.DataModel/Migrations/20260804123457_add_columns_for_v1_7.Designer.cs b/src/OneGround.ZGW.Documenten.DataModel/Migrations/20260804123457_add_columns_for_v1_7.Designer.cs new file mode 100644 index 00000000..a997d4f9 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.DataModel/Migrations/20260804123457_add_columns_for_v1_7.Designer.cs @@ -0,0 +1,1011 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OneGround.ZGW.Documenten.DataModel; + +#nullable disable + +namespace OneGround.ZGW.Documenten.DataModel.Migrations +{ + [DbContext(typeof(DrcDbContext))] + [Migration("20260804123457_add_columns_for_v1_7")] + partial class add_columns_for_v1_7 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:ci_collation", "@colStrength=secondary,@colStrength=secondary,icu,False") + .HasAnnotation("ProductVersion", "8.0.25") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pgcrypto"); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OneGround.ZGW.DataAccess.AuditTrail.AuditTrailDelta", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AanmaakDatum") + .HasColumnType("timestamp with time zone") + .HasColumnName("aanmaakdatum"); + + b.Property("Actie") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("actie"); + + b.Property("ActieWeergave") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("actieweergave"); + + b.Property("ApplicatieId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("applicatie_id"); + + b.Property("ApplicatieWeergave") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("applicatieweergave"); + + b.Property("Bron") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("bron"); + + b.Property("DeltaJson") + .HasColumnType("jsonb") + .HasColumnName("delta_json"); + + b.Property("GebruikersId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("gebruikers_id"); + + b.Property("GebruikersWeergave") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("gebruikersweergave"); + + b.Property("HoofdObject") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("hoofdobject"); + + b.Property("HoofdObjectId") + .HasColumnType("uuid") + .HasColumnName("hoofdobject_id"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("request_id"); + + b.Property("Resource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("resource"); + + b.Property("ResourceId") + .HasColumnType("uuid") + .HasColumnName("resource_id"); + + b.Property("ResourceUrl") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("resourceurl"); + + b.Property("ResourceWeergave") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("resourceweergave"); + + b.Property("Resultaat") + .HasColumnType("integer") + .HasColumnName("resultaat"); + + b.Property("SnapshotJson") + .HasColumnType("jsonb") + .HasColumnName("snapshot_json"); + + b.Property("Toelichting") + .IsRequired() + .HasColumnType("text") + .HasColumnName("toelichting"); + + b.Property("Versie") + .HasColumnType("integer") + .HasColumnName("versie"); + + b.HasKey("Id"); + + b.HasIndex("AanmaakDatum"); + + b.HasIndex("HoofdObjectId", "AanmaakDatum") + .IsDescending(false, true); + + b.HasIndex("HoofdObjectId", "ResourceId", "Versie") + .IsDescending(false, false, true); + + b.ToTable("audittrail_deltas"); + }); + + modelBuilder.Entity("OneGround.ZGW.DataAccess.AuditTrail.AuditTrailRegel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AanmaakDatum") + .HasColumnType("timestamp with time zone") + .HasColumnName("aanmaakdatum"); + + b.Property("Actie") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("actie"); + + b.Property("ActieWeergave") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("actieweergave"); + + b.Property("ApplicatieId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("applicatie_id"); + + b.Property("ApplicatieWeergave") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("applicatieweergave"); + + b.Property("Bron") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("bron"); + + b.Property("GebruikersId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("gebruikers_id"); + + b.Property("GebruikersWeergave") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("gebruikersweergave"); + + b.Property("HoofdObject") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("hoofdobject"); + + b.Property("HoofdObjectId") + .HasColumnType("uuid") + .HasColumnName("hoofdobject_id"); + + b.Property("Nieuw") + .HasColumnType("jsonb") + .HasColumnName("nieuw"); + + b.Property("Oud") + .HasColumnType("jsonb") + .HasColumnName("oud"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("request_id"); + + b.Property("Resource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("resource"); + + b.Property("ResourceUrl") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("resourceurl"); + + b.Property("ResourceWeergave") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("resourceweergave"); + + b.Property("Resultaat") + .HasColumnType("integer") + .HasColumnName("resultaat"); + + b.Property("Toelichting") + .IsRequired() + .HasColumnType("text") + .HasColumnName("toelichting"); + + b.HasKey("Id"); + + b.HasIndex("HoofdObjectId"); + + b.ToTable("audittrail"); + }); + + modelBuilder.Entity("OneGround.ZGW.DataAccess.Migrations.FinishedDataMigration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationVersion") + .HasColumnType("text") + .HasColumnName("application_version"); + + b.Property("Executed") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("finished_data_migrations"); + }); + + modelBuilder.Entity("OneGround.ZGW.DataAccess.NumberGenerator.OrganisatieNummer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("creationtime"); + + b.Property("EntiteitNaam") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entiteit"); + + b.Property("Formaat") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("formaat"); + + b.Property("HuidigNummer") + .HasColumnType("bigint") + .HasColumnName("huidig_nummer"); + + b.Property("HuidigNummerEntiteit") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("huidig_nummer_entiteit"); + + b.Property("ModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("modificationtime"); + + b.Property("Rsin") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasColumnName("rsin"); + + b.HasKey("Id"); + + b.ToTable("organisatie_nummers"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.Authorization.TempInformatieObjectAuthorization", b => + { + b.Property("InformatieObjectType") + .HasColumnType("text"); + + b.Property("MaximumVertrouwelijkheidAanduiding") + .HasColumnType("integer"); + + b.HasKey("InformatieObjectType"); + + b.ToTable("TempInformatieObjectAuthorization", null, t => + { + t.ExcludeFromMigrations(); + }); + + b.ToView("TempInformatieObjectAuthorization", (string)null); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.BestandsDeel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("EnkelvoudigInformatieObjectVersieId") + .HasColumnType("uuid") + .HasColumnName("enkelvoudiginformatieobjectversie_id"); + + b.Property("Omvang") + .HasColumnType("integer") + .HasColumnName("omvang"); + + b.Property("UploadPartId") + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("uploadpart_id"); + + b.Property("Volgnummer") + .HasColumnType("integer") + .HasColumnName("volgnummer"); + + b.Property("Voltooid") + .HasColumnType("boolean") + .HasColumnName("voltooid"); + + b.HasKey("Id"); + + b.HasIndex("EnkelvoudigInformatieObjectVersieId"); + + b.ToTable("bestandsdelen"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CatalogusId") + .HasColumnType("uuid") + .HasColumnName("catalogus_id"); + + b.Property("CreatedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("createdby"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("creationtime"); + + b.Property("IndicatieGebruiksrecht") + .HasColumnType("boolean") + .HasColumnName("indicatiegebruiksrecht"); + + b.Property("InformatieObjectType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("informatieobjecttype"); + + b.Property("LatestEnkelvoudigInformatieObjectVersieId") + .HasColumnType("uuid") + .HasColumnName("latest_enkelvoudiginformatieobjectversie_id"); + + b.Property("LatestVertrouwelijkheidAanduiding") + .HasColumnType("integer") + .HasColumnName("latest_vertrouwelijkheidaanduiding"); + + b.Property("LegacyAuditTrail") + .HasColumnType("boolean") + .HasColumnName("legacy_audittrail"); + + b.Property("Lock") + .HasColumnType("text") + .HasColumnName("lock"); + + b.Property("Locked") + .HasColumnType("boolean") + .HasColumnName("locked"); + + b.Property("ModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("modificationtime"); + + b.Property("ModifiedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("modifiedby"); + + b.Property("Owner") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasColumnName("owner"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("LatestEnkelvoudigInformatieObjectVersieId") + .IsUnique(); + + b.HasIndex("Owner", "Id") + .HasDatabaseName("t3b_IX_eio_owner_id_incl_type_latest_vha"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Owner", "Id"), new[] { "InformatieObjectType", "LatestVertrouwelijkheidAanduiding" }); + + b.HasIndex("Owner", "CreationTime", "Id") + .IsDescending(false, true, false) + .HasDatabaseName("t3b_IX_eio_owner_creationtime_id_incl_type_vha"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Owner", "CreationTime", "Id"), new[] { "InformatieObjectType", "LatestVertrouwelijkheidAanduiding" }); + + b.HasIndex("Owner", "InformatieObjectType", "LatestVertrouwelijkheidAanduiding") + .HasDatabaseName("t3b_IX_eio_owner_iot_latest_vha"); + + b.ToTable("enkelvoudiginformatieobjecten"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObjectVersie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Auteur") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("auteur"); + + b.Property("BeginRegistratie") + .HasColumnType("timestamp with time zone") + .HasColumnName("beginregistratie"); + + b.Property("Beschrijving") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("beschrijving"); + + b.Property("Bestandsnaam") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("bestandsnaam"); + + b.Property("Bestandsomvang") + .HasColumnType("bigint") + .HasColumnName("bestandsomvang"); + + b.Property("Bronorganisatie") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("bronorganisatie"); + + b.Property("CreatedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("createdby"); + + b.Property("CreatieDatum") + .HasColumnType("date") + .HasColumnName("creatiedatum"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("creationtime"); + + b.Property("EnkelvoudigInformatieObjectId") + .HasColumnType("uuid") + .HasColumnName("enkelvoudiginformatieobject_id"); + + b.Property("Formaat") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("formaat"); + + b.Property("Identificatie") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("identificatie"); + + b.Property("Inhoud") + .HasColumnType("text") + .HasColumnName("inhoud"); + + b.Property("InhoudIsVervallen") + .HasColumnType("boolean") + .HasColumnName("inhoud_is_vervallen"); + + b.Property("Integriteit_Algoritme") + .HasColumnType("integer") + .HasColumnName("integriteit_algoritme"); + + b.Property("Integriteit_Datum") + .HasColumnType("date") + .HasColumnName("integriteit_datum"); + + b.Property("Integriteit_Waarde") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("integriteit_waarde"); + + b.Property("IsGereedVoorPublicatie") + .HasColumnType("boolean") + .HasColumnName("is_gereed_voor_publicatie"); + + b.Property("Link") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("link"); + + b.Property("ModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("modificationtime"); + + b.Property("ModifiedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("modifiedby"); + + b.Property("MultiPartDocumentId") + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("multipartdocument_id"); + + b.Property("Ondertekening_Datum") + .HasColumnType("date") + .HasColumnName("ondertekening_datum"); + + b.Property("Ondertekening_Soort") + .HasMaxLength(50) + .HasColumnType("integer") + .HasColumnName("ondertekening_soort"); + + b.Property("OntvangstDatum") + .HasColumnType("date") + .HasColumnName("ontvangstdatum"); + + b.Property("Owner") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasColumnName("owner"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Taal") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("taal"); + + b.Property("Titel") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("titel"); + + b.Property("TonenAanInitiator") + .HasColumnType("boolean") + .HasColumnName("tonen_aan_initiator"); + + b.Property>("Trefwoorden") + .HasColumnType("text[]") + .HasColumnName("trefwoorden"); + + b.Property("Verschijningsvorm") + .HasColumnType("text") + .HasColumnName("verschijningsvorm"); + + b.Property("Versie") + .HasColumnType("integer") + .HasColumnName("versie"); + + b.Property("Vertrouwelijkheidaanduiding") + .HasColumnType("integer") + .HasColumnName("vertrouwelijkheidaanduiding"); + + b.Property("VerzendDatum") + .HasColumnType("date") + .HasColumnName("verzenddatum"); + + b.HasKey("Id"); + + b.HasIndex("Bronorganisatie"); + + b.HasIndex("EnkelvoudigInformatieObjectId"); + + b.HasIndex("Id") + .HasDatabaseName("idx_e0_light_covering"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id"), new[] { "Owner", "Vertrouwelijkheidaanduiding", "EnkelvoudigInformatieObjectId", "Bronorganisatie", "Identificatie" }); + + b.HasIndex("Identificatie"); + + b.HasIndex("Inhoud"); + + b.HasIndex("Trefwoorden"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Trefwoorden"), "gin"); + + b.HasIndex("Owner", "Id", "Vertrouwelijkheidaanduiding"); + + b.HasIndex("Owner", "Identificatie", "Versie") + .IsUnique() + .HasDatabaseName("IX_eiov_owner_identificatie_versie"); + + b.HasIndex("Owner", "Inhoud", "Vertrouwelijkheidaanduiding") + .IsDescending(false, false, true); + + b.HasIndex("Owner", "EnkelvoudigInformatieObjectId", "Versie", "Vertrouwelijkheidaanduiding") + .IsDescending(false, false, true, false); + + b.HasIndex("Owner", "Inhoud", "Vertrouwelijkheidaanduiding", "EnkelvoudigInformatieObjectId") + .IsDescending(false, false, true, false) + .HasDatabaseName("IX_enkelvoudiginformatieobjectversies_owner_inhoud_vertrouwel~1") + .HasFilter("Bestandsomvang IS NOT NULL"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Owner", "Inhoud", "Vertrouwelijkheidaanduiding", "EnkelvoudigInformatieObjectId"), new[] { "Bestandsomvang" }); + + b.ToTable("enkelvoudiginformatieobjectversies"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.GebruiksRecht", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("createdby"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("creationtime"); + + b.Property("Einddatum") + .HasColumnType("timestamp with time zone") + .HasColumnName("einddatum"); + + b.Property("InformatieObjectId") + .HasColumnType("uuid") + .HasColumnName("informatieobject_id"); + + b.Property("ModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("modificationtime"); + + b.Property("ModifiedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("modifiedby"); + + b.Property("OmschrijvingVoorwaarden") + .IsRequired() + .HasColumnType("text") + .HasColumnName("omschrijvingvoorwaarden"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Startdatum") + .HasColumnType("timestamp with time zone") + .HasColumnName("startdatum"); + + b.HasKey("Id"); + + b.HasIndex("InformatieObjectId"); + + b.ToTable("gebruiksrechten"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.ObjectInformatieObject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("createdby"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("creationtime"); + + b.Property("InformatieObjectId") + .HasColumnType("uuid") + .HasColumnName("informatieobject_id"); + + b.Property("ModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("modificationtime"); + + b.Property("ModifiedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("modifiedby"); + + b.Property("Object") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("object") + .UseCollation("ci_collation"); + + b.Property("ObjectType") + .HasColumnType("integer") + .HasColumnName("objecttype"); + + b.Property("Owner") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasColumnName("owner"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("InformatieObjectId"); + + b.HasIndex("Object"); + + b.HasIndex("Object", "InformatieObjectId", "ObjectType") + .IsUnique(); + + b.ToTable("objectinformatieobjecten"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.Verzending", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AardRelatie") + .HasColumnType("smallint") + .HasColumnName("aardrelatie"); + + b.Property("Betrokkene") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("betrokkene") + .UseCollation("ci_collation"); + + b.Property("BinnenlandsCorrespondentieAdres") + .HasColumnType("jsonb") + .HasColumnName("binnenlandsCorrespondentieadres"); + + b.Property("BuitenlandsCorrespondentieAdres") + .HasColumnType("jsonb") + .HasColumnName("buitenlandsCorrespondentieadres"); + + b.Property("Contactpersoon") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("contactpersoon"); + + b.Property("ContactpersoonNaam") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("contactpersoonnaam"); + + b.Property("CorrespondentiePostadres") + .HasColumnType("jsonb") + .HasColumnName("correspondentiepostadres"); + + b.Property("CreatedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("createdby"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("creationtime"); + + b.Property("EmailAdres") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("emailadres"); + + b.Property("Faxnummer") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("faxnummer"); + + b.Property("InformatieObjectId") + .HasColumnType("uuid") + .HasColumnName("informatieobject_id"); + + b.Property("MijnOverheid") + .HasColumnType("boolean") + .HasColumnName("mijnoverheid"); + + b.Property("ModificationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("modificationtime"); + + b.Property("ModifiedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("modifiedby"); + + b.Property("Ontvangstdatum") + .HasColumnType("date") + .HasColumnName("ontvangstdatum"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Telefoonnummer") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("telefoonnummer"); + + b.Property("Toelichting") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("toelichting"); + + b.Property("Verzenddatum") + .HasColumnType("date") + .HasColumnName("verzenddatum"); + + b.HasKey("Id"); + + b.HasIndex("AardRelatie"); + + b.HasIndex("Betrokkene"); + + b.HasIndex("InformatieObjectId"); + + b.ToTable("verzendingen"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.BestandsDeel", b => + { + b.HasOne("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObjectVersie", "EnkelvoudigInformatieObjectVersie") + .WithMany("BestandsDelen") + .HasForeignKey("EnkelvoudigInformatieObjectVersieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EnkelvoudigInformatieObjectVersie"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObject", b => + { + b.HasOne("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObjectVersie", "LatestEnkelvoudigInformatieObjectVersie") + .WithOne("LatestInformatieObject") + .HasForeignKey("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObject", "LatestEnkelvoudigInformatieObjectVersieId"); + + b.Navigation("LatestEnkelvoudigInformatieObjectVersie"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObjectVersie", b => + { + b.HasOne("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObject", "InformatieObject") + .WithMany("EnkelvoudigInformatieObjectVersies") + .HasForeignKey("EnkelvoudigInformatieObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InformatieObject"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.GebruiksRecht", b => + { + b.HasOne("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObject", "InformatieObject") + .WithMany("GebruiksRechten") + .HasForeignKey("InformatieObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InformatieObject"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.ObjectInformatieObject", b => + { + b.HasOne("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObject", "InformatieObject") + .WithMany("ObjectInformatieObjecten") + .HasForeignKey("InformatieObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InformatieObject"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.Verzending", b => + { + b.HasOne("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObject", "InformatieObject") + .WithMany("Verzendingen") + .HasForeignKey("InformatieObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InformatieObject"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObject", b => + { + b.Navigation("EnkelvoudigInformatieObjectVersies"); + + b.Navigation("GebruiksRechten"); + + b.Navigation("ObjectInformatieObjecten"); + + b.Navigation("Verzendingen"); + }); + + modelBuilder.Entity("OneGround.ZGW.Documenten.DataModel.EnkelvoudigInformatieObjectVersie", b => + { + b.Navigation("BestandsDelen"); + + b.Navigation("LatestInformatieObject"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/OneGround.ZGW.Documenten.DataModel/Migrations/20260804123457_add_columns_for_v1_7.cs b/src/OneGround.ZGW.Documenten.DataModel/Migrations/20260804123457_add_columns_for_v1_7.cs new file mode 100644 index 00000000..1e229048 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.DataModel/Migrations/20260804123457_add_columns_for_v1_7.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OneGround.ZGW.Documenten.DataModel.Migrations +{ + /// + public partial class add_columns_for_v1_7 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "is_gereed_voor_publicatie", + table: "enkelvoudiginformatieobjectversies", + type: "boolean", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "tonen_aan_initiator", + table: "enkelvoudiginformatieobjectversies", + type: "boolean", + nullable: false, + defaultValue: false + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn(name: "is_gereed_voor_publicatie", table: "enkelvoudiginformatieobjectversies"); + + migrationBuilder.DropColumn(name: "tonen_aan_initiator", table: "enkelvoudiginformatieobjectversies"); + } + } +} diff --git a/src/OneGround.ZGW.Documenten.DataModel/Migrations/DrcDbContextModelSnapshot.cs b/src/OneGround.ZGW.Documenten.DataModel/Migrations/DrcDbContextModelSnapshot.cs index e820d5f2..857cb88f 100644 --- a/src/OneGround.ZGW.Documenten.DataModel/Migrations/DrcDbContextModelSnapshot.cs +++ b/src/OneGround.ZGW.Documenten.DataModel/Migrations/DrcDbContextModelSnapshot.cs @@ -467,15 +467,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Owner", "Id"), new[] { "InformatieObjectType", "LatestVertrouwelijkheidAanduiding" }); - b.HasIndex("Owner", "InformatieObjectType", "LatestVertrouwelijkheidAanduiding") - .HasDatabaseName("t3b_IX_eio_owner_iot_latest_vha"); - b.HasIndex("Owner", "CreationTime", "Id") .IsDescending(false, true, false) .HasDatabaseName("t3b_IX_eio_owner_creationtime_id_incl_type_vha"); NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Owner", "CreationTime", "Id"), new[] { "InformatieObjectType", "LatestVertrouwelijkheidAanduiding" }); + b.HasIndex("Owner", "InformatieObjectType", "LatestVertrouwelijkheidAanduiding") + .HasDatabaseName("t3b_IX_eio_owner_iot_latest_vha"); + b.ToTable("enkelvoudiginformatieobjecten"); }); @@ -562,6 +562,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(128)") .HasColumnName("integriteit_waarde"); + b.Property("IsGereedVoorPublicatie") + .HasColumnType("boolean") + .HasColumnName("is_gereed_voor_publicatie"); + b.Property("Link") .HasMaxLength(200) .HasColumnType("character varying(200)") @@ -621,6 +625,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)") .HasColumnName("titel"); + b.Property("TonenAanInitiator") + .HasColumnType("boolean") + .HasColumnName("tonen_aan_initiator"); + b.Property>("Trefwoorden") .HasColumnType("text[]") .HasColumnName("trefwoorden"); diff --git a/src/OneGround.ZGW.Documenten.DataModel/ObjectType.cs b/src/OneGround.ZGW.Documenten.DataModel/ObjectType.cs index ee76a8f8..1ddf902c 100644 --- a/src/OneGround.ZGW.Documenten.DataModel/ObjectType.cs +++ b/src/OneGround.ZGW.Documenten.DataModel/ObjectType.cs @@ -4,4 +4,5 @@ public enum ObjectType { zaak, besluit, + verzoek, } diff --git a/src/OneGround.ZGW.Documenten.Web/BusinessRules/v1/5/VerzendingBusinessRuleService.cs b/src/OneGround.ZGW.Documenten.Web/BusinessRules/v1/5/VerzendingBusinessRuleService.cs index f65fc0aa..6a94f661 100644 --- a/src/OneGround.ZGW.Documenten.Web/BusinessRules/v1/5/VerzendingBusinessRuleService.cs +++ b/src/OneGround.ZGW.Documenten.Web/BusinessRules/v1/5/VerzendingBusinessRuleService.cs @@ -8,26 +8,28 @@ namespace OneGround.ZGW.Documenten.Web.BusinessRules.v1._5; public interface IVerzendingBusinessRuleService { - bool Validate(EnkelvoudigInformatieObject enkelvoudiginformatieobject, Verzending verzending, List errors); + bool Validate(EnkelvoudigInformatieObject enkelvoudiginformatieobject, Verzending verzending, decimal version, List errors); bool Validate( EnkelvoudigInformatieObject enkelvoudiginformatieobject, Verzending existingVerzending, Guid? existingVerzendingId, + decimal version, List errors ); } public class VerzendingBusinessRuleService : IVerzendingBusinessRuleService { - public bool Validate(EnkelvoudigInformatieObject informatieobject, Verzending verzending, List errors) + public bool Validate(EnkelvoudigInformatieObject informatieobject, Verzending verzending, decimal version, List errors) { - return Validate(informatieobject, verzending, existingVerzendingId: null, errors); + return Validate(informatieobject, verzending, existingVerzendingId: null, version, errors); } public bool Validate( EnkelvoudigInformatieObject informatieobject, Verzending existingVerzending, Guid? existingVerzendingId, + decimal version, List errors ) { @@ -82,7 +84,7 @@ List errors } else if (existingVerzending.AardRelatie == AardRelatie.geadresseerde) { - if (!existingVerzending.Verzenddatum.HasValue) + if (!existingVerzending.Verzenddatum.HasValue && version <= 1.5M) { errors.Add( new ValidationError( diff --git a/src/OneGround.ZGW.Documenten.Web/Controllers/Api.cs b/src/OneGround.ZGW.Documenten.Web/Controllers/Api.cs index a6562320..ef25c4b1 100644 --- a/src/OneGround.ZGW.Documenten.Web/Controllers/Api.cs +++ b/src/OneGround.ZGW.Documenten.Web/Controllers/Api.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using OneGround.ZGW.Common.Web.Services; namespace OneGround.ZGW.Documenten.Web.Controllers; @@ -8,6 +8,7 @@ public static class Api public const string LatestVersion_1_0 = "1.0.1"; public const string LatestVersion_1_1 = "1.1.0"; public const string LatestVersion_1_5 = "1.5.0"; + public const string LatestVersion_1_7 = "1.7.0"; } public class ApiMetaData : IApiMetaData @@ -20,6 +21,7 @@ public IEnumerable SupportedVersions yield return "1.0.1"; yield return "1.1.0"; yield return "1.5.0"; + yield return "1.7.0"; } } } diff --git a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/1/EnkelvoudigInformatieObjectenController.cs b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/1/EnkelvoudigInformatieObjectenController.cs index 27cd686d..e338c80e 100644 --- a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/1/EnkelvoudigInformatieObjectenController.cs +++ b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/1/EnkelvoudigInformatieObjectenController.cs @@ -410,7 +410,7 @@ CancellationToken cancellationToken /// Internal Server Error [HttpGet(ApiRoutes.EnkelvoudigInformatieObjecten.Download, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.Download)] [Scope(AuthorizationScopes.Documenten.Read)] - [Produces("application/octet-stream")] + [Produces("application/octet-stream", "application/json")] public async Task DownloadAsync( Guid id, [FromQuery] Documenten.Contracts.v1.Queries.GetEnkelvoudigInformatieObjectQueryParameters queryParameters, diff --git a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/EnkelvoudigInformatieObjectenController.cs b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/EnkelvoudigInformatieObjectenController.cs index 80581153..c989b160 100644 --- a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/EnkelvoudigInformatieObjectenController.cs +++ b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/EnkelvoudigInformatieObjectenController.cs @@ -498,7 +498,7 @@ CancellationToken cancellationToken [HttpGet(ApiRoutes.EnkelvoudigInformatieObjecten.Download, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.Download)] [Scope(AuthorizationScopes.Documenten.Read)] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(FileStreamResult))] - [Produces("application/octet-stream")] + [Produces("application/octet-stream", "application/json")] public async Task DownloadAsync( Guid id, [FromQuery] DownloadEnkelvoudigInformatieObjectQueryParameters queryParameters, diff --git a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/GebruiksRechtenController.cs b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/GebruiksRechtenController.cs index 69e192c6..08ecc662 100644 --- a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/GebruiksRechtenController.cs +++ b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/GebruiksRechtenController.cs @@ -32,6 +32,7 @@ namespace OneGround.ZGW.Documenten.Web.Controllers.v1._5; [ApiController] [Authorize] [ZgwApiVersion(Api.LatestVersion_1_5)] +[ZgwApiVersion(Api.LatestVersion_1_7)] [Consumes("application/json")] [Produces("application/json")] public class GebruiksRechtenController : ZGWControllerBase diff --git a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/ObjectInformatieObjectenController.cs b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/ObjectInformatieObjectenController.cs index 487c2824..e7350f3b 100644 --- a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/ObjectInformatieObjectenController.cs +++ b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/ObjectInformatieObjectenController.cs @@ -32,6 +32,7 @@ namespace OneGround.ZGW.Documenten.Web.Controllers.v1._5; [ApiController] [Authorize] [ZgwApiVersion(Api.LatestVersion_1_5)] +[ZgwApiVersion(Api.LatestVersion_1_7)] [Consumes("application/json")] [Produces("application/json")] public class ObjectInformatieObjectenController : ZGWControllerBase diff --git a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/VerzendingenController.cs b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/VerzendingenController.cs index 0622eee6..4587d6fa 100644 --- a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/VerzendingenController.cs +++ b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/5/VerzendingenController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Asp.Versioning; using AutoMapper; using MediatR; using Microsoft.AspNetCore.Authorization; @@ -55,6 +56,7 @@ uit Einkelvoudiginformatieobject zijn hiermee overbodig en deprecated geworden. [ApiController] [Authorize] [ZgwApiVersion(Api.LatestVersion_1_5)] +[ZgwApiVersion(Api.LatestVersion_1_7)] [Consumes("application/json")] [Produces("application/json")] public class VerzendingenController : ZGWControllerBase @@ -248,7 +250,12 @@ public async Task AddAsync([FromBody] VerzendingRequestDto verzen var verzending = _mapper.Map(verzendingRequest); var result = await _mediator.Send( - new CreateVerzendingCommand { Verzending = verzending, InformatieObjectUrl = verzendingRequest.InformatieObject }, + new CreateVerzendingCommand + { + Verzending = verzending, + InformatieObjectUrl = verzendingRequest.InformatieObject, + Version = IsApiVersionRequested(new ApiVersion(1, 7)) ? 1.7M : 1.5M, + }, cancellationToken ); @@ -295,6 +302,7 @@ public async Task UpdateAsync([FromBody] VerzendingRequestDto ver { Id = id, InformatieObjectUrl = verzendingRequest.InformatieObject, + Version = IsApiVersionRequested(new ApiVersion(1, 7)) ? 1.7M : 1.5M, Verzending = verzending, // Note: Indicates that the versie should be fully replaced in the command handler PartialObject = null, }, @@ -348,6 +356,7 @@ public async Task PartialUpdateAsync([FromBody] JObject partialVe { Id = id, InformatieObjectUrl = GetValueFromPartial(partialVerzendingRequest, "informatieObject"), + Version = IsApiVersionRequested(new ApiVersion(1, 7)) ? 1.7M : 1.5M, Verzending = null, PartialObject = partialVerzendingRequest, // Note: Indicates that the versie should be merged in the command handler }, diff --git a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/7/EnkelvoudigInformatieObjectenController.cs b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/7/EnkelvoudigInformatieObjectenController.cs new file mode 100644 index 00000000..248a74eb --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/7/EnkelvoudigInformatieObjectenController.cs @@ -0,0 +1,702 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using AutoMapper; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OneGround.ZGW.Common.Constants; +using OneGround.ZGW.Common.Contracts.v1; +using OneGround.ZGW.Common.Handlers; +using OneGround.ZGW.Common.MimeTypes; +using OneGround.ZGW.Common.Web.Authorization; +using OneGround.ZGW.Common.Web.Controllers; +using OneGround.ZGW.Common.Web.Expands; +using OneGround.ZGW.Common.Web.Filters; +using OneGround.ZGW.Common.Web.Handlers; +using OneGround.ZGW.Common.Web.Middleware; +using OneGround.ZGW.Common.Web.Models; +using OneGround.ZGW.Common.Web.Services; +using OneGround.ZGW.Common.Web.Services.AuditTrail; +using OneGround.ZGW.Common.Web.Validations; +using OneGround.ZGW.Common.Web.Versioning; +using OneGround.ZGW.Documenten.Contracts.v1._7.Queries; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; +using OneGround.ZGW.Documenten.Contracts.v1._7.Responses; +using OneGround.ZGW.Documenten.DataModel; +using OneGround.ZGW.Documenten.Services; +using OneGround.ZGW.Documenten.Web.Authorization; +using OneGround.ZGW.Documenten.Web.Configuration; +using OneGround.ZGW.Documenten.Web.Handlers.v1._7; +using OneGround.ZGW.Documenten.Web.Models.v1._7; +using Swashbuckle.AspNetCore.Annotations; + +// DRC large files: https://vng-realisatie.github.io/gemma-zaken/ontwikkelaars/handleidingen-en-tutorials/large-files + +namespace OneGround.ZGW.Documenten.Web.Controllers.v1._7; + +[ApiController] +[Authorize] +[ZgwApiVersion(Api.LatestVersion_1_7)] +[Consumes("application/json")] +[Produces("application/json")] +public class EnkelvoudigInformatieObjectenController : ZGWControllerBase +{ + private readonly IPaginationHelper _paginationHelper; + private readonly IValidatorService _validatorService; + + //private readonly IObjectExpander _expander; // TODO: Refactor in FUND-2655 DRC 1.7 (expand/field selection) + private readonly ApplicationConfiguration _applicationConfiguration; + + public EnkelvoudigInformatieObjectenController( + ILogger logger, + IMediator mediator, + IMapper mapper, + IRequestMerger requestMerger, + IConfiguration configuration, + IPaginationHelper paginationHelper, + IErrorResponseBuilder errorResponseBuilder, + IValidatorService validatorService /* + IExpanderFactory expanderFactory*/ // TODO: Refactor in FUND-2655 DRC 1.7 (expand/field selection) + ) + : base(logger, mediator, mapper, requestMerger, errorResponseBuilder) + { + _paginationHelper = paginationHelper; + _validatorService = validatorService; + _applicationConfiguration = configuration.GetSection("Application").Get(); + // TODO: Refactor in FUND-2655 DRC 1.7 (expand/field selection) + //_expander = expanderFactory.Create("enkelvoudiginformatieobject"); + } + + // + // HTTP GET http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten + + /// + /// Alle ENKELVOUDIGEINFORMATIEOBJECTen opvragen. + /// Deze lijst kan gefilterd wordt met query-string parameters. + /// + /// Unauthorized + /// Forbidden + /// Not found + /// Too Many Requests + /// Internal Server Error + [HttpGet(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.GetAll, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.List)] + [Scope(AuthorizationScopes.Documenten.Read)] + [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(PagedResponse))] + [Expand] + [ServiceFilter(typeof(ValidateQueryParametersFilter))] + public async Task GetAllAsync( + [FromQuery] GetAllEnkelvoudigInformatieObjectenQueryParameters queryParameters, + int page = 1, + CancellationToken cancellationToken = default + ) + { + _logger.LogDebug("{ControllerMethod} called with {@FromQuery}, {Page}", nameof(GetAllAsync), queryParameters, page); + + var pagination = _mapper.Map(new PaginationQuery(page, _applicationConfiguration.EnkelvoudigInformatieObjectenPageSize)); + var filter = _mapper.Map(queryParameters); + + var result = await _mediator.Send( + new GetAllEnkelvoudigInformatieObjectenQuery { GetAllEnkelvoudigInformatieObjectenFilter = filter, Pagination = pagination }, + cancellationToken + ); + + if (!_paginationHelper.ValidatePaginatedResponse(pagination, result.Result.Count)) + { + return _errorResponseBuilder.PageNotFound(); + } + + var enkelvoudigInformatieObjectenResponse = _mapper.Map>(result.Result.PageResult); + + var expandLookup = ExpandLookup(queryParameters.Expand); + + var enkelvoudigInformatieObjectenWithOptionalExpand = enkelvoudigInformatieObjectenResponse + //.Select(e => _expander.ResolveAsync(expandLookup, e).Result) // TODO: Refactor in FUND-2655 DRC 1.7 (expand/field selection) + .ToList(); + + var paginationResponse = _paginationHelper.CreatePaginatedResponse( + queryParameters, + pagination, + enkelvoudigInformatieObjectenWithOptionalExpand, + result.Result.Count + ); + + await _mediator.Send( + new LogAuditTrailGetObjectListCommand + { + RetrieveCatagory = RetrieveCatagory.Minimal, + Page = pagination.Page, + Count = paginationResponse.Results.Count(), + TotalCount = paginationResponse.Count, + AuditTrailOptions = new AuditTrailOptions { Bron = ServiceRoleName.DRC, Resource = "enkelvoudiginformatieobject" }, + }, + cancellationToken + ); + + return Ok(paginationResponse); + } + + // + // HTTP GET http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten/b24ee37c-00db-4108-b831-e3b420b35a09 + // HTTP GET http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten/b24ee37c-00db-4108-b831-e3b420b35a09?versie=2®istratieop=2020-11-07 + + /// + /// Een specifieke ENKELVOUDIGEINFORMATIEOBJECT opvragen. + /// + /// Not Modified + /// Unauthorized + /// Forbidden + /// Not found + /// Too Many Requests + /// Internal Server Error + [HttpGet(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Get, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.Read)] + [Scope(AuthorizationScopes.Documenten.Read)] + [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(EnkelvoudigInformatieObjectGetResponseExpandedDto))] + [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] + [ETagFilter] + [Expand] + public async Task GetAsync( + Guid id, + [FromQuery] Documenten.Contracts.v1._5.Queries.GetEnkelvoudigInformatieObjectQueryParameters queryParameters, + CancellationToken cancellationToken + ) + { + _logger.LogDebug("{ControllerMethod} called with {Uuid}, {@FromQuery}", nameof(GetAsync), id, queryParameters); + + var filter = _mapper.Map(queryParameters); + + var result = await _mediator.Send( + new GetEnkelvoudigInformatieObjectQuery { Id = id, GetEnkelvoudigInformatieObjectFilter = filter }, + cancellationToken + ); + + if (result.Status == QueryStatus.NotFound) + { + return _errorResponseBuilder.NotFound(); + } + + if (result.Status == QueryStatus.Forbidden) + { + return _errorResponseBuilder.Forbidden(); + } + + var enkelvoudigInformatieObject = _mapper.Map(result.Result); + + var expandLookup = ExpandLookup(queryParameters.Expand); + + // TODO: Refactor in FUND-2655 DRC 1.7 (expand/field selection) + //var enkelvoudigInformatieObjectWithOptionalExpand = await _expander.ResolveAsync(expandLookup, enkelvoudigInformatieObject); + + await _mediator.Send( + new LogAuditTrailGetObjectCommand + { + RetrieveCatagory = RetrieveCatagory.Minimal, + BaseEntity = result.Result, + SubEntity = result.Result, + AuditTrailOptions = new AuditTrailOptions { Bron = ServiceRoleName.DRC, Resource = "enkelvoudiginformatieobject" }, + LegacyAuditTrail = result.Result.LegacyAuditTrail, + }, + cancellationToken + ); + + return Ok( /*enkelvoudigInformatieObjectWithOptionalExpand*/ // TODO: Refactor in FUND-2655 DRC 1.7 (expand/field selection) + enkelvoudigInformatieObject + ); + } + + /// + /// De headers voor een specifiek(e) ENKELVOUDIG INFORMATIE OBJECT opvragen. + /// + /// OK + /// Not Modified + /// Unauthorized + /// Forbidden + /// Not found + /// Too Many Requests + /// Internal Server Error + [HttpHead(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Get, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.ReadHead)] + [Scope(AuthorizationScopes.Documenten.Read)] + [ETagFilter] + [Expand] + public Task HeadAsync( + Guid id, + [FromQuery] Documenten.Contracts.v1._5.Queries.GetEnkelvoudigInformatieObjectQueryParameters queryParameters, + CancellationToken cancellationToken + ) + { + return GetAsync(id, queryParameters, cancellationToken); + } + + // + // HTTP POST http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten/_zoek + + /// + /// Voer een zoekopdracht uit op (ENKELVOUDIG) INFORMATIEOBJECTen. + /// + /// + /// Zoeken/filteren gaat normaal via de list operatie, deze is echter niet geschikt voor zoekopdrachten met UUIDs. + /// + /// Unauthorized + /// Forbidden + /// Not found + /// Too Many Requests + /// Internal Server Error + [HttpPost(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Search, Name = Contracts.v1._5.Operations.EnkelvoudigInformatieObjecten.Search)] + [Scope(AuthorizationScopes.Documenten.Read)] + [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(PagedResponse))] + [Expand] + [ServiceFilter(typeof(ValidateBodyParametersFilter))] + public async Task SearchAsync( + [FromBody] EnkelvoudigInformatieObjectSearchRequestDto enkelvoudiginformatieobjectSearchRequest, + int page = 1, + CancellationToken cancellationToken = default + ) + { + _logger.LogDebug("{ControllerMethod} called with {@FromBody}, {Page}", nameof(SearchAsync), enkelvoudiginformatieobjectSearchRequest, page); + + var pagination = _mapper.Map(new PaginationQuery(page, _applicationConfiguration.EnkelvoudigInformatieObjectenPageSize)); + var filter = _mapper.Map(enkelvoudiginformatieobjectSearchRequest); + + var result = await _mediator.Send( + new GetAllEnkelvoudigInformatieObjectenQuery { GetAllEnkelvoudigInformatieObjectenFilter = filter, Pagination = pagination }, + cancellationToken + ); + + if (!_paginationHelper.ValidatePaginatedResponse(pagination, result.Result.Count)) + { + return _errorResponseBuilder.PageNotFound(); + } + + var enkelvoudigInformatieObjectenResponse = _mapper.Map>(result.Result.PageResult); + + var expandLookup = ExpandLookup(enkelvoudiginformatieobjectSearchRequest.Expand); + + var enkelvoudigInformatieObjectenWithOptionalExpand = enkelvoudigInformatieObjectenResponse + // .Select(e => _expander.ResolveAsync(expandLookup, e).Result) // TODO: Refactor in FUND-2655 DRC 1.7 (expand/field selection) + .ToList(); + + var paginationResponse = _paginationHelper.CreatePaginatedResponse( + pagination, + enkelvoudigInformatieObjectenWithOptionalExpand, + result.Result.Count + ); + + await _mediator.Send( + new LogAuditTrailGetObjectListCommand + { + RetrieveCatagory = RetrieveCatagory.Minimal, + Page = pagination.Page, + Count = paginationResponse.Results.Count(), + TotalCount = paginationResponse.Count, + AuditTrailOptions = new AuditTrailOptions { Bron = ServiceRoleName.DRC, Resource = "enkelvoudiginformatieobject" }, + }, + cancellationToken + ); + + return Ok(paginationResponse); + } + + // + // HTTP POST http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten + + /// + /// Maak een (ENKELVOUDIG) INFORMATIEOBJECT aan. + /// + /// Unauthorized + /// Forbidden + /// Too Many Requests + /// Internal Server Error + [HttpPost(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Create, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.Create)] + [Scope(AuthorizationScopes.Documenten.Create)] + [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] + [SwaggerResponse(StatusCodes.Status201Created, Type = typeof(EnkelvoudigInformatieObjectCreateResponseDto))] + public async Task AddAsync( + [FromBody] EnkelvoudigInformatieObjectCreateRequestDto enkelvoudigInformatieObjectRequest, + CancellationToken cancellationToken + ) + { + _logger.LogDebug( + "{ControllerMethod} called with {@FromBody}, {Rsin}", + nameof(AddAsync), + enkelvoudigInformatieObjectRequest, + enkelvoudigInformatieObjectRequest.Bronorganisatie + ); + + var enkelvoudigInformatieObjectVersie = _mapper.Map(enkelvoudigInformatieObjectRequest); + + // Note: we should investigate who send the 2-letter language code so we log for these situations + LogInvalidTaalCode(enkelvoudigInformatieObjectRequest.Taal, enkelvoudigInformatieObjectVersie.Taal); + + var result = await _mediator.Send( + new CreateEnkelvoudigInformatieObjectCommand { EnkelvoudigInformatieObjectVersie = enkelvoudigInformatieObjectVersie }, + cancellationToken + ); + + if (result.Status == CommandStatus.Forbidden) + { + return _errorResponseBuilder.Forbidden(); + } + + if (result.Status == CommandStatus.ValidationError) + { + return _errorResponseBuilder.BadRequest(result.Errors); + } + + var enkelvoudigInformatieObjectResponse = _mapper.Map(result.Result); + + return Created(enkelvoudigInformatieObjectResponse.Url, enkelvoudigInformatieObjectResponse); + } + + // + // HTTP PUT http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten/59bad509-840b-4cd0-82dc-cbda74a75c2b + + /// + /// Werk een (ENKELVOUDIG) INFORMATIEOBJECT in zijn geheel bij. + /// + /// Unauthorized + /// Forbidden + /// Not found + /// EnkelvoudigInformatieObject was modified by another user + /// Too Many Requests + /// Internal Server Error + [HttpPut(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Update, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.Update)] + [Scope(AuthorizationScopes.Documenten.Update)] + [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] + [SwaggerResponse(StatusCodes.Status409Conflict, Type = typeof(ErrorResponse))] + [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(EnkelvoudigInformatieObjectUpdateResponseDto))] + public async Task UpdateAsync( + [FromBody] EnkelvoudigInformatieObjectUpdateRequestDto enkelvoudigInformatieObjectRequest, + Guid id, + CancellationToken cancellationToken + ) + { + _logger.LogDebug( + "{ControllerMethod} called with {@FromBody}, {Rsin}", + nameof(UpdateAsync), + enkelvoudigInformatieObjectRequest, + enkelvoudigInformatieObjectRequest.Bronorganisatie + ); + + EnkelvoudigInformatieObjectVersie enkelvoudigInformatieObjectVersie = _mapper.Map( + enkelvoudigInformatieObjectRequest + ); + + // Note: we should investigate who send the 2-letter language code so we log for these situations + LogInvalidTaalCode(enkelvoudigInformatieObjectRequest.Taal, enkelvoudigInformatieObjectVersie.Taal); + + var result = await _mediator.Send( + new UpdateEnkelvoudigInformatieObjectCommand + { + ExistingEnkelvoudigInformatieObjectId = id, + EnkelvoudigInformatieObjectVersie = enkelvoudigInformatieObjectVersie, // Note: Indicates that the versie should be fully replaced in the command handler + PartialObject = null, + }, + cancellationToken + ); + + if (result.Status == CommandStatus.NotFound) + { + return _errorResponseBuilder.NotFound(); + } + + if (result.Status == CommandStatus.Forbidden) + { + return _errorResponseBuilder.Forbidden(); + } + + if (result.Status == CommandStatus.ValidationError) + { + return _errorResponseBuilder.BadRequest(result.Errors); + } + + if (result.Status == CommandStatus.Conflict) + { + return _errorResponseBuilder.Conflict(result.Errors); + } + + var enkelvoudigInformatieObjectResponse = _mapper.Map(result.Result); + + return Ok(enkelvoudigInformatieObjectResponse); + } + + // + // HTTP PATCH http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten/59bad509-840b-4cd0-82dc-cbda74a75c2b + + /// + /// Werk een (ENKELVOUDIG) INFORMATIEOBJECT deels bij. + /// + /// Unauthorized + /// Forbidden + /// Not found + /// EnkelvoudigInformatieObject was modified by another user + /// Too Many Requests + /// Internal Server Error + [HttpPatch( + Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Update, + Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.PartialUpdate + )] + [Scope(AuthorizationScopes.Documenten.Update)] + [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] + [SwaggerResponse(StatusCodes.Status409Conflict, Type = typeof(ErrorResponse))] + [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(EnkelvoudigInformatieObjectUpdateResponseDto))] + public async Task PartialUpdateAsync( + [FromBody] dynamic partialEnkelvoudigInformatieObjectRequest, + Guid id, + CancellationToken cancellationToken + ) + { + // We do log only the request not the partial update request (because can be large) + _logger.LogDebug("{ControllerMethod} called with {Uuid}", nameof(PartialUpdateAsync), id); + + var result = await _mediator.Send( + new UpdateEnkelvoudigInformatieObjectCommand + { + ExistingEnkelvoudigInformatieObjectId = id, + EnkelvoudigInformatieObjectVersie = null, + PartialObject = partialEnkelvoudigInformatieObjectRequest, // Note: Indicates that the versie should be merged in the command handler + }, + cancellationToken + ); + + if (result.Status == CommandStatus.NotFound) + { + return _errorResponseBuilder.NotFound(); + } + + if (result.Status == CommandStatus.Forbidden) + { + return _errorResponseBuilder.Forbidden(); + } + + if (result.Status == CommandStatus.ValidationError) + { + return _errorResponseBuilder.BadRequest(result.Errors); + } + + if (result.Status == CommandStatus.Conflict) + { + return _errorResponseBuilder.Conflict(result.Errors); + } + + var enkelvoudigInformatieObjectResponse = _mapper.Map(result.Result); + + return Ok(enkelvoudigInformatieObjectResponse); + } + + // + // HTTP GET http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten/b24ee37c-00db-4108-b831-e3b420b35a09/download + // HTTP GET http://documenten.user.local:5007/api/v1/enkelvoudiginformatieobjecten/b24ee37c-00db-4108-b831-e3b420b35a09/download?versie=2 + + /// + /// Download de binaire data van het (ENKELVOUDIG) INFORMATIEOBJECT.. + /// + /// OK + /// Unauthorized + /// Forbidden + /// Not found + /// Too Many Requests + /// Internal Server Error + [HttpGet(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Download, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.Download)] + [Scope(AuthorizationScopes.Documenten.Read)] + [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(FileStreamResult))] + [Produces("application/octet-stream", "application/json")] + public async Task DownloadAsync( + Guid id, + [FromQuery] Documenten.Contracts.v1._5.Queries.DownloadEnkelvoudigInformatieObjectQueryParameters queryParameters, + CancellationToken cancellationToken + ) + { + _logger.LogDebug("{ControllerMethod} called with {Uuid}, {@FromQuery}", nameof(DownloadAsync), id, queryParameters); + + var filter = _mapper.Map(queryParameters); + + var resultGet = await _mediator.Send( + new GetEnkelvoudigInformatieObjectQuery + { + Id = id, + GetEnkelvoudigInformatieObjectFilter = filter, + IgnoreNotCompletedDocuments = true, + }, + cancellationToken + ); + + if (resultGet.Status == QueryStatus.NotFound) + { + return _errorResponseBuilder.NotFound(); + } + + if (resultGet.Status == QueryStatus.Forbidden) + { + return _errorResponseBuilder.Forbidden(); + } + + var enkelvoudigInformatieObjectVersie = resultGet.Result.LatestEnkelvoudigInformatieObjectVersie; // Note: Latest is mapped to the requested version from GetEnkelvoudigInformatieObjectQuery (so it could be a different one) + + // Note: New in v1.1: if file size = 0, i.e.EnkelvoudigInformatieObject contains only metadata without file content.The EnkelvoudigInformatieObject is created using a single request to Documenten API. + if (enkelvoudigInformatieObjectVersie.Bestandsomvang == 0 && enkelvoudigInformatieObjectVersie.Inhoud == null) + { + if (_applicationConfiguration.DocumentJobPrioritizationAtDownload) + { + await _mediator.Send( + new Handlers.v1._1.PrioritizationDocumentJobCommand + { + EnkelvoudigInformatieObjectId = enkelvoudigInformatieObjectVersie.EnkelvoudigInformatieObjectId, + }, + cancellationToken + ); + } + // Document meta does exists but no content + return NoContent(); // TODO: Unclear what to return. ZGW reference does respond with HTTP Status 500. I think 204 [NoContent] make sense here + } + + var documentUrn = new DocumentUrn(enkelvoudigInformatieObjectVersie.Inhoud); + + var resultDwnl = await _mediator.Send( + new Handlers.v1._5.DownloadEnkelvoudigInformatieObjectQuery + { + DocumentUrn = documentUrn, + EnkelvoudigInformatieObjectId = enkelvoudigInformatieObjectVersie.EnkelvoudigInformatieObjectId, + }, + cancellationToken + ); + + if (resultDwnl.Status == QueryStatus.NotFound) + { + return _errorResponseBuilder.NotFound(); + } + + await _mediator.Send( + new LogAuditTrailGetObjectCommand + { + RetrieveCatagory = RetrieveCatagory.Minimal, + BaseEntity = resultGet.Result, + SubEntity = enkelvoudigInformatieObjectVersie, + OverruleActieWeergave = "Object gedownload", + AuditTrailOptions = new AuditTrailOptions { Bron = ServiceRoleName.DRC, Resource = "enkelvoudiginformatieobject" }, + LegacyAuditTrail = resultGet.Result.LegacyAuditTrail, + }, + cancellationToken + ); + + var cd = new ContentDisposition { FileName = HttpUtility.UrlPathEncode(enkelvoudigInformatieObjectVersie.Bestandsnaam) }; + + Response.Headers.ContentDisposition = cd.ToString(); + Response.Headers.XContentTypeOptions = "nosniff"; + + var mimeType = string.IsNullOrEmpty(enkelvoudigInformatieObjectVersie.Formaat) + ? MimeTypeHelper.GetMimeType(enkelvoudigInformatieObjectVersie.Bestandsnaam) + : enkelvoudigInformatieObjectVersie.Formaat; + + return File(resultDwnl.Result, mimeType); + } + + /// + /// Vergrendel een (ENKELVOUDIG) INFORMATIEOBJECT. + /// Voert een "checkout" uit waardoor het (ENKELVOUDIG) INFORMATIEOBJECT vergrendeld wordt met een lock waarde. + /// Alleen met deze waarde kan het (ENKELVOUDIG) INFORMATIEOBJECT bijgewerkt (PUT, PATCH) en weer ontgrendeld worden. + /// + /// Unauthorized + /// Forbidden + /// Not found + /// EnkelvoudigInformatieObject was modified by another user + /// Too Many Requests + /// Internal Server Error + [HttpPost(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Lock, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.Lock)] + [Scope(AuthorizationScopes.Documenten.Lock)] + [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] + [SwaggerResponse(StatusCodes.Status409Conflict, Type = typeof(ErrorResponse))] + [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(Documenten.Contracts.v1.Responses.LockResponseDto))] + public async Task LockAsync(Guid id, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new LockEnkelvoudigInformatieObjectCommand { Id = id, Set = true }, cancellationToken); + + if (result.Status == CommandStatus.NotFound) + { + return _errorResponseBuilder.NotFound(); + } + + if (result.Status == CommandStatus.Forbidden) + { + return _errorResponseBuilder.Forbidden(); + } + + if (result.Status == CommandStatus.ValidationError) + { + return _errorResponseBuilder.BadRequest(result.Errors); + } + + if (result.Status == CommandStatus.Conflict) + { + return _errorResponseBuilder.Conflict(result.Errors); + } + + var response = new Documenten.Contracts.v1.Responses.LockResponseDto { Lock = result.Result }; + + return Ok(response); + } + + // + // HTTP POST https://documenten-api.vng.cloud/api/v1/enkelvoudiginformatieobjecten/b24ee37c-00db-4108-b831-e3b420b35a09/unlock + + /// + /// Ontgrendel een (ENKELVOUDIG) INFORMATIEOBJECT. + /// Heft de "checkout" op waardoor het (ENKELVOUDIG) INFORMATIEOBJECT ontgrendeld wordt. + /// + /// No content + /// Unauthorized + /// Forbidden + /// Not found + /// EnkelvoudigInformatieObject was modified by another user + /// Too Many Requests + /// Internal Server Error + [HttpPost(Contracts.v1._5.ApiRoutes.EnkelvoudigInformatieObjecten.Unlock, Name = Contracts.v1.Operations.EnkelvoudigInformatieObjecten.Unlock)] + [Scope(AuthorizationScopes.Documenten.Lock, AuthorizationScopes.Documenten.ForcedUnlock)] + [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] + [SwaggerResponse(StatusCodes.Status409Conflict, Type = typeof(ErrorResponse))] + [IgnoreMissingContentType] + public async Task UnlockAsync( + Guid id, + [FromBody] Documenten.Contracts.v1.Requests.LockRequestDto request, + CancellationToken cancellationToken + ) + { + var result = await _mediator.Send( + new LockEnkelvoudigInformatieObjectCommand + { + Id = id, + Set = false, + Lock = request?.Lock, + }, + cancellationToken + ); + + if (result.Status == CommandStatus.NotFound) + { + return _errorResponseBuilder.NotFound(); + } + + if (result.Status == CommandStatus.Forbidden) + { + return _errorResponseBuilder.Forbidden(); + } + + if (result.Status == CommandStatus.ValidationError) + { + return _errorResponseBuilder.BadRequest(result.Errors); + } + + if (result.Status == CommandStatus.Conflict) + { + return _errorResponseBuilder.Conflict(result.Errors); + } + + return NoContent(); + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/EnkelvoudigInformatieObjectenController.cs b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/EnkelvoudigInformatieObjectenController.cs index 3d8b2fbd..83d5665e 100644 --- a/src/OneGround.ZGW.Documenten.Web/Controllers/v1/EnkelvoudigInformatieObjectenController.cs +++ b/src/OneGround.ZGW.Documenten.Web/Controllers/v1/EnkelvoudigInformatieObjectenController.cs @@ -385,7 +385,7 @@ CancellationToken cancellationToken /// Internal Server Error [HttpGet(ApiRoutes.EnkelvoudigInformatieObjecten.Download, Name = Operations.EnkelvoudigInformatieObjecten.Download)] [Scope(AuthorizationScopes.Documenten.Read)] - [Produces("application/octet-stream")] + [Produces("application/octet-stream", "application/json")] public async Task DownloadAsync( Guid id, [FromQuery] GetEnkelvoudigInformatieObjectQueryParameters queryParameters, @@ -591,6 +591,7 @@ public async Task UnlockAsync(Guid id, [FromBody] LockRequestDto [ZgwApiVersion(Api.LatestVersion_1_0)] [ZgwApiVersion(Api.LatestVersion_1_1)] [ZgwApiVersion(Api.LatestVersion_1_5)] + [ZgwApiVersion(Api.LatestVersion_1_7)] public async Task DeleteAsync(Guid id, CancellationToken cancellationToken) { _logger.LogDebug("{ControllerMethod} called with {Uuid}", nameof(DeleteAsync), id); diff --git a/src/OneGround.ZGW.Documenten.Web/Extensions/QueryParameterValidationExtensions.cs b/src/OneGround.ZGW.Documenten.Web/Extensions/QueryAndSearchParameterValidationsExtensions.cs similarity index 56% rename from src/OneGround.ZGW.Documenten.Web/Extensions/QueryParameterValidationExtensions.cs rename to src/OneGround.ZGW.Documenten.Web/Extensions/QueryAndSearchParameterValidationsExtensions.cs index 06fe6a7e..456c5b85 100644 --- a/src/OneGround.ZGW.Documenten.Web/Extensions/QueryParameterValidationExtensions.cs +++ b/src/OneGround.ZGW.Documenten.Web/Extensions/QueryAndSearchParameterValidationsExtensions.cs @@ -3,13 +3,15 @@ namespace OneGround.ZGW.Documenten.Web.Extensions; -public static class QueryParameterValidationExtensions +public static class QueryAndSearchParameterValidationsExtensions { - public static void AddQueryParameterValidations(this IServiceCollection services) + public static void AddQueryAndSearchParameterValidations(this IServiceCollection services) { // Register the query parameter validation filters globally for v1.0 .. v1.x + services.AddScoped>(); services.AddScoped>(); services.AddScoped>(); + services.AddScoped>(); services.AddScoped>(); services.AddScoped>(); @@ -17,5 +19,9 @@ public static void AddQueryParameterValidations(this IServiceCollection services services.AddScoped>(); services.AddScoped>(); + + // Register the body parameter validation filters globally for v1.0 .. v1.x (HTTP POST /_zoek) + services.AddScoped>(); + services.AddScoped>(); } } diff --git a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/5/CreateVerzendingCommandHandler.cs b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/5/CreateVerzendingCommandHandler.cs index 4c4752f3..dcc8fa25 100644 --- a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/5/CreateVerzendingCommandHandler.cs +++ b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/5/CreateVerzendingCommandHandler.cs @@ -84,7 +84,7 @@ public async Task> Handle(CreateVerzendingCommand requ return new CommandResult(null, CommandStatus.Forbidden); } - _verzendingBusinessRuleService.Validate(informatieObject, request.Verzending, errors); + _verzendingBusinessRuleService.Validate(informatieObject, request.Verzending, request.Version, errors); if (errors.Count != 0) { @@ -119,4 +119,5 @@ class CreateVerzendingCommand : IRequest> { public Verzending Verzending { get; internal set; } public string InformatieObjectUrl { get; internal set; } + public decimal Version { get; internal set; } } diff --git a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/5/UpdateVerzendingCommandHandler.cs b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/5/UpdateVerzendingCommandHandler.cs index fe73ab99..b48624b5 100644 --- a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/5/UpdateVerzendingCommandHandler.cs +++ b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/5/UpdateVerzendingCommandHandler.cs @@ -122,7 +122,7 @@ public async Task> Handle(UpdateVerzendingCommand requ return new CommandResult(null, CommandStatus.Forbidden); } - _verzendingBusinessRuleService.Validate(existingVerzending.InformatieObject, verzending, request.Id, errors); + _verzendingBusinessRuleService.Validate(existingVerzending.InformatieObject, verzending, request.Id, request.Version, errors); if (errors.Count != 0) { @@ -165,6 +165,7 @@ class UpdateVerzendingCommand : IRequest> { public Guid Id { get; internal set; } public string InformatieObjectUrl { get; internal set; } + public decimal Version { get; internal set; } public Verzending Verzending { get; internal set; } // For PUT endpoint, contains the full update sent by the client public dynamic PartialObject { get; internal set; } // For PATCH endpoint, contains the partial update sent by the client } diff --git a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/CreateEnkelvoudigInformatieObjectCommandHandler.cs b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/CreateEnkelvoudigInformatieObjectCommandHandler.cs new file mode 100644 index 00000000..1cef5023 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/CreateEnkelvoudigInformatieObjectCommandHandler.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Npgsql; +using OneGround.ZGW.Catalogi.ServiceAgent.v1; +using OneGround.ZGW.Common.Contracts; +using OneGround.ZGW.Common.Contracts.v1; +using OneGround.ZGW.Common.Handlers; +using OneGround.ZGW.Common.Web.Authorization; +using OneGround.ZGW.Common.Web.Services; +using OneGround.ZGW.Common.Web.Services.AuditTrail; +using OneGround.ZGW.Common.Web.Services.UriServices; +using OneGround.ZGW.Documenten.Contracts.v1._7.Responses; +using OneGround.ZGW.Documenten.DataModel; +using OneGround.ZGW.Documenten.Services; +using OneGround.ZGW.Documenten.Web.Authorization; +using OneGround.ZGW.Documenten.Web.BusinessRules.v1; +using OneGround.ZGW.Documenten.Web.Extensions; +using OneGround.ZGW.Documenten.Web.Notificaties; +using OneGround.ZGW.Documenten.Web.Services; + +namespace OneGround.ZGW.Documenten.Web.Handlers.v1._7; + +public class CreateEnkelvoudigInformatieObjectCommandHandler + : _5.MutatieEnkelvoudigInformatieObjectCommandHandler, + IRequestHandler> +{ + public CreateEnkelvoudigInformatieObjectCommandHandler( + ILogger logger, + IConfiguration configuration, + DrcDbContext context, + IEntityUriService uriService, + INummerGenerator nummerGenerator, + IDocumentServicesResolver documentServicesResolver, + IEnkelvoudigInformatieObjectBusinessRuleService enkelvoudigInformatieObjectBusinessRuleService, + ICatalogiServiceAgent catalogiServiceAgent, + IAuditTrailFactory auditTrailFactory, + IAuthorizationContextAccessor authorizationContextAccessor, + ILockGenerator lockGenerator, + IOptions formOptions, + INotificatieService notificatieService, + IDocumentKenmerkenResolver documentKenmerkenResolver + ) + : base( + logger, + configuration, + uriService, + authorizationContextAccessor, + documentServicesResolver, + context, + enkelvoudigInformatieObjectBusinessRuleService, + nummerGenerator, + catalogiServiceAgent, + auditTrailFactory, + lockGenerator, + formOptions, + notificatieService, + documentKenmerkenResolver + ) { } + + public async Task> Handle( + CreateEnkelvoudigInformatieObjectCommand request, + CancellationToken cancellationToken + ) + { + _logger.LogDebug("Creating EnkelvoudigInformatieObject...."); + + var versie = request.EnkelvoudigInformatieObjectVersie; + + if ( + !_authorizationContext.IsAuthorized( + request.EnkelvoudigInformatieObjectVersie.InformatieObject.InformatieObjectType, + request.EnkelvoudigInformatieObjectVersie.Vertrouwelijkheidaanduiding, + AuthorizationScopes.Documenten.Create + ) + ) + { + return new CommandResult(null, CommandStatus.Forbidden); + } + + versie.SetLinkToNullWhenInvalid(); + versie.EscapeBestandsNaamWhenInvalid(); + + var errors = new List(); + + // Create the new (initial) version of the EnkelvoudigInformatieObject + versie.Versie = 1; + versie.InformatieObject.Owner = _rsin; + versie.BeginRegistratie = DateTime.UtcNow; + versie.Owner = _rsin; + + await _enkelvoudigInformatieObjectBusinessRuleService.ValidateAsync( + versie, + _applicationConfiguration.IgnoreInformatieObjectTypeValidation, + existingEnkelvoudigInformatieObjectId: null, + isPartialUpdate: false, + apiVersie: 1.7M, + errors, + cancellationToken + ); + + if (errors.Count != 0) + { + return new CommandResult(null, CommandStatus.ValidationError, errors.ToArray()); + } + + var informatieobjecttype = await _catalogiServiceAgent.GetInformatieObjectTypeByUrlAsync( + request.EnkelvoudigInformatieObjectVersie.InformatieObject.InformatieObjectType + ); + if (!informatieobjecttype.Success) + { + return new CommandResult( + null, + CommandStatus.ValidationError, + new ValidationError("enkelvoudiginformatieobjecttype", informatieobjecttype.Error.Code, informatieobjecttype.Error.Title) + ); + } + var catalogusId = _uriService.GetId(informatieobjecttype.Response.Catalogus); + + // Note: Vertrouwelijkheidaanduiding van een informatieobject (drc-007) => get from request or get from Catalogi.InformatieObjectType + await SetVertrouwelijkheidAanduidingAsync(request.EnkelvoudigInformatieObjectVersie); + + using (var audittrail = _auditTrailFactory.Create(AuditTrailOptions, legacy: false)) + { + // Depending on the specified inhoud and bestandsomvang several ways on how to add documents.... + if (IsDocumentUploadWithBestandsdelen(versie.Bestandsomvang, versie.Inhoud)) + { + // We have enabled (some) metadata fields for the underlying document provider + var metadata = new DocumentMeta { Rsin = versie.InformatieObject.Owner, Version = versie.Versie }; + + var result = await DocumentService.InitiateMultipartUploadAsync(versie.Bestandsnaam, metadata, cancellationToken); + + versie.MultiPartDocumentId = result.Context; + + AddBestandsDelenToEnkelvoudigeInformatieObjectVersie(versie); + + versie.Inhoud = null; + } + else if (IsDocumentMetaOnly(versie.Bestandsomvang, versie.Inhoud)) + { + versie.Bestandsomvang = 0; + versie.Inhoud = null; + } + else + { + var (inhoud, bestandsomvang) = await TryAddDocumentToDocumentStore(versie, errors, cancellationToken); + if (errors.Count != 0) + { + return new CommandResult(null, CommandStatus.ValidationError, errors.ToArray()); + } + versie.Inhoud = inhoud; + versie.Bestandsomvang = bestandsomvang; + } + + // Use external identificatie if specified generate otherwise + if (string.IsNullOrEmpty(versie.Identificatie)) + { + var owner = request.EnkelvoudigInformatieObjectVersie.Owner; + + var enkelvoudigInformatieObjectNummer = await _nummerGenerator.GenerateAsync( + owner, + "documenten", + id => IsEnkelvoudigInformatieObjectVersieUnique(owner, id), + cancellationToken + ); + + versie.Identificatie = enkelvoudigInformatieObjectNummer; + } + + await _context.EnkelvoudigInformatieObjectVersies.AddAsync(versie, cancellationToken); // Note: Sequential Guid for Id is generated here by the DBMS + + using var trans = await _context.Database.BeginTransactionAsync(cancellationToken); + + // Saves the new added EnkelvoudigInformationObject and EnkelvoudigInformationObjectVersion. + // Handle potential race condition on INSERT with unique constraint in Postgres: + // if two requests hit this API simultaneously, a "check-then-act" pattern can fail and the INSERT may violate the unique constraint. + try + { + // Try to save changes with potential concurrency conflict + await _context.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx && pgEx.SqlState == PostgresErrorCodes.UniqueViolation) + { + // Handle preventing race-condition where another process has created already EnkelvoudigInformatieObject with the same identificatie+owner+versie after our initial read but before our insert + await RollbackDocumentJustAddedToDmsAsync(versie.Inhoud); + + var error = new ValidationError("identificatie", ErrorCode.Unique, "Deze identificatie bestaat al voor deze organisatie."); + return new CommandResult(null, CommandStatus.ValidationError, error); + } + catch (Exception) + { + // Rollback and re-throw the exception to be handled by the global exception handler, but first try to rollback the document just added to DMS if any + await RollbackDocumentJustAddedToDmsAsync(versie.Inhoud); + throw; + } + + // Sets the 'latest' EnkelvoudigInformationObjectVersion in the parent EnkelvoudigInformatieObject + versie.InformatieObject.LatestEnkelvoudigInformatieObjectVersieId = versie.Id; + versie.InformatieObject.LatestEnkelvoudigInformatieObjectVersie = versie; + versie.InformatieObject.LatestVertrouwelijkheidAanduiding = versie.Vertrouwelijkheidaanduiding; + + versie.LatestInformatieObject = versie.InformatieObject; + + versie.InformatieObject.CatalogusId = catalogusId; + + audittrail.SetNew(versie.InformatieObject); + + await audittrail.CreatedAsync(versie.InformatieObject, versie.InformatieObject, cancellationToken); + + await _context.SaveChangesAsync(cancellationToken); + + await trans.CommitAsync(cancellationToken); + } + + _logger.LogDebug("EnkelvoudigInformatieObject {Id} successfully created", versie.InformatieObject.Id); + + // Note: Do not send notification using bestandsdelen (this is done when all bestandsdelen are uploaded an checkin is called) + if (versie.BestandsDelen.Count == 0) + { + await SendNotificationAsync(Actie.create, versie.InformatieObject, cancellationToken); // TODO: Check older versions how this reacts!!!!! + } + + return new CommandResult(versie, CommandStatus.OK); + } + + private async Task RollbackDocumentJustAddedToDmsAsync(string urnDocument) + { + if (string.IsNullOrEmpty(urnDocument)) + { + return; + } + + try + { + await DocumentService.DeleteDocumentAsync(new DocumentUrn(urnDocument)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to rollback document with urn {UrnDocument} just added to DMS", urnDocument); + // Do not throw an exception on the rollback failure + } + } +} + +public class CreateEnkelvoudigInformatieObjectCommand : IRequest> +{ + public EnkelvoudigInformatieObjectVersie EnkelvoudigInformatieObjectVersie { get; internal set; } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/GetAllEnkelvoudigInformatieObjectenQueryHandler.cs b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/GetAllEnkelvoudigInformatieObjectenQueryHandler.cs new file mode 100644 index 00000000..2dad225c --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/GetAllEnkelvoudigInformatieObjectenQueryHandler.cs @@ -0,0 +1,465 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Npgsql; +using OneGround.ZGW.Common.Caching; +using OneGround.ZGW.Common.Handlers; +using OneGround.ZGW.Common.Helpers; +using OneGround.ZGW.Common.Web.Authorization; +using OneGround.ZGW.Common.Web.Models; +using OneGround.ZGW.Common.Web.Services.UriServices; +using OneGround.ZGW.Documenten.DataModel; +using OneGround.ZGW.Documenten.DataModel.Authorization; +using OneGround.ZGW.Documenten.Web.Models.v1._7; +using OneGround.ZGW.Documenten.Web.Services; + +namespace OneGround.ZGW.Documenten.Web.Handlers.v1._7; + +class GetAllEnkelvoudigInformatieObjectenQueryHandler + : DocumentenBaseHandler, + IRequestHandler>> +{ + private readonly DrcDbContext _context; + private readonly IDistributedCacheHelper _cache; + private readonly IInformatieObjectAuthorizationTempTableService _informatieObjectAuthorizationTempTableService; + private readonly IConfiguration _configuration; + + // TTL for cached count/page anchors. Count/Anchors for a given (rsin, page, filter) tuple are stable within this window. + private static readonly TimeSpan CountCacheLifetime = TimeSpan.FromMinutes(5); + private static readonly TimeSpan AnchorCacheLifetime = TimeSpan.FromMinutes(5); + + // Returned as the total count when the count query exceeds the command timeout on very large tenants. + // The unfiltered COUNT over the full owner partition can run for several seconds; rather than failing the + // whole request, we return this sentinel so the caller still gets a page. The real count reappears once the + // query completes within the timeout (e.g. after caches warm up); the sentinel is deliberately not cached. + private const int CountTimeoutSentinel = 999_999_999; + + public GetAllEnkelvoudigInformatieObjectenQueryHandler( + ILogger logger, + IConfiguration configuration, + IEntityUriService uriService, + DrcDbContext context, + IAuthorizationContextAccessor authorizationContextAccessor, + IDistributedCacheHelper cache, + IInformatieObjectAuthorizationTempTableService informatieObjectAuthorizationTempTableService, + IDocumentKenmerkenResolver documentKenmerkenResolver + ) + : base(logger, configuration, uriService, authorizationContextAccessor, documentKenmerkenResolver) + { + _context = context; + _cache = cache; + _informatieObjectAuthorizationTempTableService = informatieObjectAuthorizationTempTableService; + _configuration = configuration; + } + + public async Task>> Handle( + GetAllEnkelvoudigInformatieObjectenQuery request, + CancellationToken cancellationToken + ) + { + _logger.LogDebug("Get all EnkelvoudigInformatieObjecten...."); + + var filter = GetEnkelvoudigInformatieObjectFilterPredicate(request.GetAllEnkelvoudigInformatieObjectenFilter); + + var rsinFilter = GetRsinFilterPredicate(); + + bool alleenIsGereedVoorPublicatie = _authorizationContext.Authorization.AlleenIsGereedVoorPublicatie; + + var query = _context + .EnkelvoudigInformatieObjecten.AsNoTracking() + .Include(e => e.LatestEnkelvoudigInformatieObjectVersie) + .Where(rsinFilter) + .Where(filter); + + if (alleenIsGereedVoorPublicatie) + { + query = query.Where(e => + e.LatestEnkelvoudigInformatieObjectVersie.IsGereedVoorPublicatie.HasValue + && alleenIsGereedVoorPublicatie == e.LatestEnkelvoudigInformatieObjectVersie.IsGereedVoorPublicatie + ); + } + + bool hasAuthorizationFilter = !_authorizationContext.Authorization.HasAllAuthorizations; + if (hasAuthorizationFilter) + { + await _informatieObjectAuthorizationTempTableService.InsertInformatieObjectTypeAuthorizationsToTempTableAsync( + _authorizationContext, + _context, + cancellationToken + ); + + // Fetch auth pairs into C# and build an inline predicate grouped by VHA level. + // Any temp-table reference in the paginated query causes PostgreSQL to rewrite it as a + // semi-join where the temp table becomes the outer loop — forcing a Sort of all ~1M matching + // rows before LIMIT takes effect (214s). Inlining the data removes the join so PostgreSQL + // can use the sorted (owner, creationtime, id) index with early termination via LIMIT. + // Pairs are grouped by MaxVha: each group becomes iot = ANY(@types) AND vha <= maxVha. + // This collapses N pairs (up to 5000) into ≤7 OR terms (one per ZGW VHA level), keeping + // the predicate cheap to plan and execute regardless of how many types are authorized. + var authPairs = await _context.TempInformatieObjectAuthorization.AsNoTracking().ToListAsync(cancellationToken); + + query = query.Where(BuildInlineAuthorizationPredicate(authPairs)); + } + + // Count with authorization reuses the same query (it already carries the inline VHA-grouped + // auth predicate), so no temp-table JOIN is needed. Without a selectivity filter the planner + // may pick a BitmapOr that turns lossy at scale and triggers millions of heap rechecks (~29s); + // GetAuthorizationCountCachedAsync forces enable_bitmapscan=off so it uses the + // (owner, iot, vha) covering index as a single Index-Only Scan + aggregate — O(N), no heap. + int totalCount; + try + { + totalCount = hasAuthorizationFilter + ? await GetAuthorizationCountCachedAsync(query, request.GetAllEnkelvoudigInformatieObjectenFilter, cancellationToken) + : await GetTotalCountCachedAsync(query, request.GetAllEnkelvoudigInformatieObjectenFilter, cancellationToken); + } + catch (Exception ex) when (IsCountTimeout(ex, cancellationToken)) + { + // Don't fail the whole listing because the count is too expensive on this tenant — return a + // sentinel count and still serve the page. The factory threw before caching, so nothing poisoned + // the cache and the next request retries the real count. + _logger.LogWarning(ex, "Count query timed out for rsin {Rsin}; returning sentinel count {Sentinel}.", _rsin, CountTimeoutSentinel); + totalCount = CountTimeoutSentinel; + } + + // Default 'enable_sort=off' but for some filters disabling sort will drop performance significantly + var filterModel = request.GetAllEnkelvoudigInformatieObjectenFilter; + + var enableSort = + !string.IsNullOrEmpty(filterModel.Identificatie) || (filterModel.Trefwoorden_In?.Any() ?? false) || (filterModel.Uuid_In?.Any() ?? false) + ? "on" + : "off"; + + var cmd = $"SET LOCAL enable_sort = {enableSort};"; + + // SET LOCAL requires an active transaction. Settings revert automatically when + // the transaction is disposed — safe for pooled connections. Must be set before Phase 1 + // runs, since that's the query whose plan (sort vs. sorted-index scan) this controls. + List pageIds; + await using (var transaction = await _context.Database.BeginTransactionAsync(cancellationToken)) + { + await _context.Database.ExecuteSqlRawAsync(cmd, cancellationToken); + + // Phase 1: Get page IDs using a narrow SELECT so the planner uses the sorted + // (owner, creationtime, id) covering index (t3b_IX_eio_owner_creationtime_id_incl_type_vha) + // with early termination, instead of materializing all matching rows. + pageIds = + _configuration.GetValue("Application:EnkelvoudigInformatieObjectenCursorPaging") ?? true + ? await GetAnchorPagedResult(request, query, cancellationToken) + : await GetOffsetPagedResult(request, query, cancellationToken); + } + + // Phase 2: Fetch complete data for only the matched IDs (typically 100 PK lookups). + var pagedResult = + pageIds.Count > 0 + ? await OrderByPage( + _context + .EnkelvoudigInformatieObjecten.AsNoTracking() + .Where(e => pageIds.Contains(e.Id)) + .Include(e => e.LatestEnkelvoudigInformatieObjectVersie) + .ThenInclude(e => e.BestandsDelen) + ) + .ToListAsync(cancellationToken) + : []; + + var result = new PagedResult { PageResult = pagedResult, Count = totalCount }; + + return new QueryResult>(result, QueryStatus.OK); + } + + private static async Task> GetOffsetPagedResult( + GetAllEnkelvoudigInformatieObjectenQuery request, + IQueryable query, + CancellationToken cancellationToken + ) + { + return await OrderByPage(query) + .Skip(request.Pagination.Size * (request.Pagination.Page - 1)) + .Take(request.Pagination.Size) + .Select(e => e.Id) + .ToListAsync(cancellationToken); + } + + // Single source of truth for the page ordering. The keyset seek predicate in + // GetAnchorPagedResult is only correct while every ORDER BY matches this exact tuple, + // so all paging paths (offset, seek, and the Phase-2 fetch) must order through here. + private static IOrderedQueryable OrderByPage(IQueryable query) => + query.OrderByDescending(e => e.CreationTime).ThenBy(e => e.Id); + + private async Task> GetAnchorPagedResult( + GetAllEnkelvoudigInformatieObjectenQuery request, + IQueryable query, + CancellationToken cancellationToken + ) + { + var filterHash = ObjectHasher.ComputeSha1Hash(request.GetAllEnkelvoudigInformatieObjectenFilter); + + PageAnchor? anchor = null; + if (request.Pagination.Page > 1) + { + var previousAnchorKey = $"anchors:{_rsin}:{request.Pagination.Page - 2}:{filterHash}"; + + // Look up the previous page's composite anchor in the distributed cache. + // A sentinel (PageAnchor.Sentinel) is stored when no real anchor is available yet, + // so the factory isn't re-invoked on subsequent lookups within the TTL. If a deep + // page is requested before page 1, it falls back to the offset method until the + // sentinel expires — functionally correct, just less optimal. + var cachedAnchor = await _cache.GetAsync( + previousAnchorKey, + factory: () => Task.FromResult(PageAnchor.Sentinel), + absoluteExpirationRelativeToNow: AnchorCacheLifetime, + cancellationToken + ); + + if (cachedAnchor != PageAnchor.Sentinel) + anchor = cachedAnchor; + } + + bool useSeekMethod = anchor != null; + + IQueryable orderedQuery; + if (useSeekMethod) + { + // Seek method: keyset pagination using (CreationTime, Id) as the composite anchor. + // Rows "after" the anchor in (CreationTime DESC, Id ASC) order are exactly those matching + // the OR-form: CreationTime < anchor.CreationTime + // OR (CreationTime == anchor.CreationTime AND Id > anchor.Id) + // + // That OR-form alone can never be a btree index-range bound (the mixed sort directions rule + // out a row-comparison rewrite), so PostgreSQL keeps it as a Filter and starts the scan at + // the top of the index — deep pages then re-scan everything newer than the anchor (offset- + // like cost). The leading "CreationTime <= anchor.CreationTime" conjunct fixes that: it is + // logically implied by both OR branches (so results are unchanged) but IS sargable, so + // PostgreSQL uses it as an Index Cond to start the scan at the anchor. The OR-form then only + // disambiguates the boundary rows (same CreationTime, Id tiebreak). + orderedQuery = OrderByPage( + query.Where(e => + e.CreationTime <= anchor!.CreationTime + && (e.CreationTime < anchor.CreationTime || (e.CreationTime == anchor.CreationTime && e.Id > anchor.Id)) + ) + ) + .Take(request.Pagination.Size); + } + else + { + // Offset method: traditional Skip/Take fallback when no anchor is cached. + orderedQuery = OrderByPage(query).Skip(request.Pagination.Size * (request.Pagination.Page - 1)).Take(request.Pagination.Size); + } + + // Phase 1 selects both Id and CreationTime — both are needed to build the composite anchor. + var pageData = await orderedQuery.Select(e => new { e.Id, e.CreationTime }).ToListAsync(cancellationToken); + + var result = pageData.ConvertAll(x => x.Id); + + if (result.Count > 0 && (!useSeekMethod || result.Count == request.Pagination.Size)) + { + var last = pageData[^1]; + var currentAnchorKey = $"anchors:{_rsin}:{request.Pagination.Page - 1}:{filterHash}"; + + // GetAsync uses get-or-add semantics — if a key already exists within the TTL the + // existing entry is kept. The anchor for a given (rsin, page, filter) is stable + // within the TTL window, so this is acceptable. + await _cache.GetAsync( + currentAnchorKey, + factory: () => Task.FromResult(new PageAnchor(last.CreationTime, last.Id)), + absoluteExpirationRelativeToNow: AnchorCacheLifetime, + cancellationToken + ); + } + + return result; + } + + // Composite keyset anchor for cursor-based pagination on (CreationTime DESC, Id ASC). + // Sentinel is stored in cache to prevent repeated factory calls when no real anchor exists yet. + private sealed record PageAnchor(DateTime CreationTime, Guid Id) + { + public static readonly PageAnchor Sentinel = new(DateTime.MinValue, Guid.Empty); + } + + private async Task GetAuthorizationCountCachedAsync( + IQueryable query, + GetAllEnkelvoudigInformatieObjectenFilter filterModel, + CancellationToken cancellationToken + ) + { + var key = ObjectHasher.ComputeSha1Hash(new { Rsin = _rsin, GetAllEnkelvoudigInformatieObjectenFilter = filterModel }); + + return await _cache.GetAsync( + key, + factory: async () => + { + // The query already carries the inline VHA-grouped auth predicate, so this is just a + // COUNT over it — no temp-table JOIN. Unlike the paged query there is no LIMIT to + // terminate early, so without a selectivity filter (~1M rows) the planner may pick a + // BitmapOr that turns lossy and triggers millions of heap rechecks (~29s). Forcing + // enable_bitmapscan=off makes it use the (owner, iot, vha) covering index as a single + // Index-Only Scan + filter + aggregate. With a filter the result is small enough that + // the planner chooses correctly on its own. + bool anyFiltersSet = + !string.IsNullOrEmpty(filterModel.Identificatie) + || !string.IsNullOrEmpty(filterModel.Bronorganisatie) + || (filterModel.Uuid_In != null && filterModel.Uuid_In.Any()) + || (filterModel.Trefwoorden_In != null && filterModel.Trefwoorden_In.Any()) + || !string.IsNullOrEmpty(filterModel.ObjectInformatieObjecten_Object) + || !string.IsNullOrEmpty(filterModel.ObjectInformatieObjecten_ObjectType); + if (!anyFiltersSet) + { + // SET LOCAL requires an active transaction. Settings revert automatically when + // the transaction is disposed — safe for pooled connections. + using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken); + await _context.Database.ExecuteSqlRawAsync("SET LOCAL enable_bitmapscan = off;", cancellationToken); + + return await query.CountAsync(cancellationToken); + } + + return await query.CountAsync(cancellationToken); + }, + absoluteExpirationRelativeToNow: CountCacheLifetime, + cancellationToken + ); + } + + private async Task GetTotalCountCachedAsync( + IQueryable query, + GetAllEnkelvoudigInformatieObjectenFilter filter, + CancellationToken cancellationToken + ) + { + // Create a key for the current request+Rsin (uri contains the query-parameters as well) + var key = ObjectHasher.ComputeSha1Hash(new { Rsin = _rsin, GetAllEnkelvoudigInformatieObjectenFilter = filter }); + + // Note: Cache the Count from SQL for 5 minutes to avoid repeated expensive queries until the cache expires. + int totalCount = await _cache.GetAsync( + key, + factory: async () => + { + return await query.CountAsync(cancellationToken); + }, + absoluteExpirationRelativeToNow: CountCacheLifetime, + cancellationToken + ); + + return totalCount; + } + + // True only for a database command timeout on the count query — never for a genuine caller/client + // cancellation, which must keep bubbling up. A client-side Npgsql command timeout surfaces as an + // exception chain containing a TimeoutException; a server-side statement_timeout surfaces as a + // PostgresException with SqlState 57014 (query_canceled). + private static bool IsCountTimeout(Exception ex, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return false; + + for (var current = ex; current != null; current = current.InnerException) + { + if (current is TimeoutException) + return true; + if (current is PostgresException { SqlState: PostgresErrorCodes.QueryCanceled }) + return true; + } + + return false; + } + + // Builds: (iot = ANY(@types_for_vha5) AND vha <= 5) OR (iot = ANY(@types_for_vha3) AND vha <= 3) OR ... + // Groups N auth pairs by MaxVha so the predicate has ≤7 OR terms (one per ZGW VHA level) regardless + // of how many types are authorized. List.Contains() translates to = ANY(ARRAY[...]) in PostgreSQL — + // a single operator, efficient for large type sets. NULL LatestVertrouwelijkheidAanduiding evaluates + // to UNKNOWN in SQL → row excluded, matching the old JOIN behavior. + private static Expression> BuildInlineAuthorizationPredicate( + List authPairs + ) + { + var param = Expression.Parameter(typeof(EnkelvoudigInformatieObject), "o"); + + if (authPairs.Count == 0) + return Expression.Lambda>(Expression.Constant(false), param); + + // Order groups by VHA and types within each group so the generated SQL is deterministic + // regardless of the (unordered) temp-table fetch order — stable SQL text => plan-cache reuse. + var grouped = authPairs + .GroupBy(p => p.MaximumVertrouwelijkheidAanduiding) + .OrderBy(g => g.Key) + .Select(g => (MaxVha: g.Key, Types: g.Select(p => p.InformatieObjectType).OrderBy(t => t).ToList())) + .ToList(); + + var iotProp = Expression.Property(param, nameof(EnkelvoudigInformatieObject.InformatieObjectType)); + var vhaNullable = Expression.Property(param, nameof(EnkelvoudigInformatieObject.LatestVertrouwelijkheidAanduiding)); + var vhaInt = Expression.Convert(Expression.Property(vhaNullable, "Value"), typeof(int)); + var containsMethod = typeof(List).GetMethod(nameof(List.Contains))!; + + Expression? body = null; + foreach (var (maxVha, types) in grouped) + { + var containsExpr = Expression.Call(Expression.Constant(types), containsMethod, iotProp); + var vhaLe = Expression.LessThanOrEqual(vhaInt, Expression.Constant(maxVha)); + var groupExpr = Expression.AndAlso(containsExpr, vhaLe); + body = body == null ? groupExpr : Expression.OrElse(body, groupExpr); + } + + return Expression.Lambda>(body!, param); + } + + private static Expression> GetEnkelvoudigInformatieObjectFilterPredicate( + GetAllEnkelvoudigInformatieObjectenFilter filter + ) + { + var filterUuid_In = filter + .Uuid_In?.Select(uuid => Guid.TryParse(uuid, out var parsedUuid) ? parsedUuid : (Guid?)null) + .Where(parsedUuid => parsedUuid != null) + .Select(parsedUuid => parsedUuid!.Value) + .ToList(); + + var filterTrefwoorden_In = filter.Trefwoorden_In?.ToList(); + + ObjectType? filterObjectInformatieObjecten_ObjectType = null; + if (filter.ObjectInformatieObjecten_ObjectType != null) + { + filterObjectInformatieObjecten_ObjectType = Enum.Parse(filter.ObjectInformatieObjecten_ObjectType, ignoreCase: true); + } + // Only reference the versie navigation when versie-based filters are actually set. + // This avoids an unnecessary LEFT JOIN that prevents the planner from using the + // sorted (owner, creationtime, id) covering index with early termination. + + bool hasVersieFilters = + filter.Bronorganisatie != null + || filter.Identificatie != null + || filterTrefwoorden_In != null + || filterObjectInformatieObjecten_ObjectType != null + || filter.ObjectInformatieObjecten_Object != null; + + if (!hasVersieFilters) + { + return e => filterUuid_In == null || !filterUuid_In.Any() || filterUuid_In.Contains(e.Id); + } + + return e => + (filter.Bronorganisatie == null || e.LatestEnkelvoudigInformatieObjectVersie.Bronorganisatie == filter.Bronorganisatie) + && (filter.Identificatie == null || e.LatestEnkelvoudigInformatieObjectVersie.Identificatie == filter.Identificatie) + && (filterUuid_In == null || filterUuid_In.Contains(e.Id)) + && (filterTrefwoorden_In == null || e.LatestEnkelvoudigInformatieObjectVersie.Trefwoorden.Any(t => filterTrefwoorden_In.Contains(t))) + && ( + filter.ObjectInformatieObjecten_Object == null + || e.ObjectInformatieObjecten.Any(o => o.Object == filter.ObjectInformatieObjecten_Object) + ) + && ( + filterObjectInformatieObjecten_ObjectType == null + || e.ObjectInformatieObjecten.Any(o => o.ObjectType == filterObjectInformatieObjecten_ObjectType) + ); + } +} + +class GetAllEnkelvoudigInformatieObjectenQuery : IRequest>> +{ + public GetAllEnkelvoudigInformatieObjectenFilter GetAllEnkelvoudigInformatieObjectenFilter { get; internal set; } + public PaginationFilter Pagination { get; internal init; } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/GetEnkelvoudigInformatieObjectQueryHandler.cs b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/GetEnkelvoudigInformatieObjectQueryHandler.cs new file mode 100644 index 00000000..137ca23c --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/GetEnkelvoudigInformatieObjectQueryHandler.cs @@ -0,0 +1,151 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OneGround.ZGW.Common.Handlers; +using OneGround.ZGW.Common.Web.Authorization; +using OneGround.ZGW.Common.Web.Services.UriServices; +using OneGround.ZGW.Documenten.DataModel; +using OneGround.ZGW.Documenten.Web.Authorization; +using OneGround.ZGW.Documenten.Web.Models.v1; + +namespace OneGround.ZGW.Documenten.Web.Handlers.v1._7; + +class GetEnkelvoudigInformatieObjectQueryHandler + : DocumentenBaseHandler, + IRequestHandler> +{ + private readonly DrcDbContext _context; + + public GetEnkelvoudigInformatieObjectQueryHandler( + ILogger logger, + IConfiguration configuration, + IEntityUriService uriService, + DrcDbContext context, + IAuthorizationContextAccessor authorizationContextAccessor, + IDocumentKenmerkenResolver documentKenmerkenResolver + ) + : base(logger, configuration, uriService, authorizationContextAccessor, documentKenmerkenResolver) + { + _context = context; + } + + public async Task> Handle( + GetEnkelvoudigInformatieObjectQuery request, + CancellationToken cancellationToken + ) + { + _logger.LogDebug("Get EnkelvoudigInformatieObject {Id}....", request.Id); + + // Note: The object contains metadata about the document and the download link (content) to the binary data. + // By default, this returns the latest version of the (SINGLE) INFORMATION OBJECT. Specific versions can + // be requested by means of query string parameters. + + var filter = request.GetEnkelvoudigInformatieObjectFilter; + var rsinFilterVersion = GetRsinFilterPredicate(); + var notCompletedFilter = GetNotCompletedDocumentFilterExpression(request.IgnoreNotCompletedDocuments); + + // Build a single query that fetches both the parent object and the correct version + IQueryable versieQuery = _context + .EnkelvoudigInformatieObjectVersies.AsNoTracking() + .AsSplitQuery() + .Where(rsinFilterVersion) + .Where(v => v.EnkelvoudigInformatieObjectId == request.Id) + .Where(notCompletedFilter) + .Include(v => v.LatestInformatieObject) + .Include(v => v.BestandsDelen) + .Include(v => v.InformatieObject); // Include parent object in one query + + bool alleenIsGereedVoorPublicatie = _authorizationContext.Authorization.AlleenIsGereedVoorPublicatie; + if (alleenIsGereedVoorPublicatie) + { + // Filter out versions that are not ready for publication + versieQuery = versieQuery.Where(v => v.IsGereedVoorPublicatie.HasValue && alleenIsGereedVoorPublicatie == v.IsGereedVoorPublicatie); + } + + // Apply version filtering logic + EnkelvoudigInformatieObjectVersie requestedVersie; + + if (!filter.Versie.HasValue && !filter.RegistratieOp.HasValue) + { + // Latest version (Filter: none) + requestedVersie = await versieQuery.OrderBy(v => v.Versie).LastOrDefaultAsync(cancellationToken); + } + else if (filter.Versie.HasValue && !filter.RegistratieOp.HasValue) + { + // Specific version (Filter: 'versie') + requestedVersie = await versieQuery.Where(v => v.Versie == filter.Versie).SingleOrDefaultAsync(cancellationToken); + } + else if (!filter.Versie.HasValue && filter.RegistratieOp.HasValue) + { + // Nearest version before date (Filter: 'registratieOp') + requestedVersie = await versieQuery + .Where(v => v.BeginRegistratie <= filter.RegistratieOp) + .OrderBy(v => v.BeginRegistratie) + .LastOrDefaultAsync(cancellationToken); + } + else + { + // Both filters (Filter: 'registratieOp' and 'versie') + requestedVersie = await versieQuery + .Where(v => v.BeginRegistratie <= filter.RegistratieOp && v.Versie == filter.Versie) + .SingleOrDefaultAsync(cancellationToken); + } + + if (requestedVersie == null) + { + return new QueryResult(null, QueryStatus.NotFound); + } + + var enkelvoudiginformatieobject = requestedVersie.InformatieObject; + + // Authorization check on the requested version + if (!AuthorizationContextAccessor.AuthorizationContext.IsAuthorized(requestedVersie)) + { + return new QueryResult(null, QueryStatus.Forbidden); + } + + // Set up the relationships (both directions) + enkelvoudiginformatieobject.LatestEnkelvoudigInformatieObjectVersie = requestedVersie; + enkelvoudiginformatieobject.LatestEnkelvoudigInformatieObjectVersie.LatestInformatieObject = enkelvoudiginformatieobject; + + // Attach only the requested version + enkelvoudiginformatieobject.EnkelvoudigInformatieObjectVersies.Clear(); + enkelvoudiginformatieobject.EnkelvoudigInformatieObjectVersies.Add(requestedVersie); + + if (request.IgnoreLock) + { + enkelvoudiginformatieobject.Lock = null; + } + + return new QueryResult(enkelvoudiginformatieobject, QueryStatus.OK); + } + + private static Expression> GetNotCompletedDocumentFilterExpression(bool ignoreNotCompletedDocuments) + { + if (!ignoreNotCompletedDocuments) + { + // No filter, always true + return v => true; + } + else + { + // Only include versions with no BestandsDelen + return v => v.BestandsDelen.Count == 0; + } + } +} + +class GetEnkelvoudigInformatieObjectQuery : IRequest> +{ + public Guid Id { get; internal set; } + public GetEnkelvoudigInformatieObjectFilter GetEnkelvoudigInformatieObjectFilter { get; internal set; } = + new GetEnkelvoudigInformatieObjectFilter(); + public bool IgnoreLock { get; internal set; } + public bool IgnoreNotCompletedDocuments { get; internal set; } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/LockEnkelvoudigInformatieObjectCommandHandler.cs b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/LockEnkelvoudigInformatieObjectCommandHandler.cs new file mode 100644 index 00000000..902c7f01 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/LockEnkelvoudigInformatieObjectCommandHandler.cs @@ -0,0 +1,234 @@ +using System; +using System.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OneGround.ZGW.Common.Constants; +using OneGround.ZGW.Common.Contracts.v1; +using OneGround.ZGW.Common.Handlers; +using OneGround.ZGW.Common.Web.Authorization; +using OneGround.ZGW.Common.Web.Services; +using OneGround.ZGW.Common.Web.Services.AuditTrail; +using OneGround.ZGW.Common.Web.Services.UriServices; +using OneGround.ZGW.DataAccess; +using OneGround.ZGW.Documenten.Contracts.v1._7.Responses; +using OneGround.ZGW.Documenten.DataModel; +using OneGround.ZGW.Documenten.Services; +using OneGround.ZGW.Documenten.Web.Authorization; +using OneGround.ZGW.Documenten.Web.Concurrency; + +namespace OneGround.ZGW.Documenten.Web.Handlers.v1._7; + +public class LockEnkelvoudigInformatieObjectCommandHandler + : DocumentenBaseHandler, + IRequestHandler> +{ + private readonly DrcDbContext _context; + private readonly IAuditTrailFactory _auditTrailFactory; + private readonly IDocumentService _documentService; + private readonly ResilienceConcurrencyRetryPipeline _concurrencyRetryPipeline; + + public LockEnkelvoudigInformatieObjectCommandHandler( + ILogger logger, + IConfiguration configuration, + DrcDbContext context, + IEntityUriService uriService, + IAuditTrailFactory auditTrailFactory, + IAuthorizationContextAccessor authorizationContextAccessor, + INotificatieService notificatieService, + IDocumentServicesResolver documentServicesResolver, + IDocumentKenmerkenResolver documentKenmerkenResolver, + ResilienceConcurrencyRetryPipeline concurrencyRetryPipeline + ) + : base(logger, configuration, uriService, authorizationContextAccessor, notificatieService, documentKenmerkenResolver) + { + _context = context; + _auditTrailFactory = auditTrailFactory; + _concurrencyRetryPipeline = concurrencyRetryPipeline; + + _documentService = documentServicesResolver.GetDefault(); + } + + public async Task> Handle(LockEnkelvoudigInformatieObjectCommand request, CancellationToken cancellationToken) + { + if (request.Set) + _logger.LogDebug("Locking EnkelvoudigInformatieObject...."); + else + _logger.LogDebug("Unlocking EnkelvoudigInformatieObject...."); + + // Use ReadCommitted isolation level: + // - FOR UPDATE provides pessimistic row-level locking (prevents concurrent modifications) + // - xmin (configured in EnkelvoudigInformatieObject) provides optimistic concurrency detection (detects any changes since read) + // - ReadCommitted allows better concurrency than Serializable for this use case + // - The combination prevents both lost updates (via FOR UPDATE) and write skew (via xmin) + using var tx = await _context.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); + + var rsinFilter = GetRsinFilterPredicate(); + + var (enkelvoudigInformatieObject, status) = await _concurrencyRetryPipeline.ExecuteWithResultAsync( + async (token) => + { + // First, try to acquire lock on the EnkelvoudigInformatieObject + var _enkelvoudigInformatieObject = await _context + .EnkelvoudigInformatieObjecten.LockForUpdate(_context, c => c.Id, [request.Id]) + .Where(rsinFilter) + .Include(e => e.LatestEnkelvoudigInformatieObjectVersie) + .ThenInclude(v => v.BestandsDelen) // Note: Important here to keep audittrail consistent with the actual locked/unlocked state of the document (see handling of incompleted multi-part documents in the code below) + .SingleOrDefaultAsync(e => e.Id == request.Id, token); + + // The object might be locked OR not exist - check if it exists without lock + if (_enkelvoudigInformatieObject == null) + { + // The object might be locked OR not exist - check if it exists without lock + var exists = await _context.EnkelvoudigInformatieObjecten.Where(rsinFilter).AnyAsync(e => e.Id == request.Id, token); + + if (!exists) + { + // Object truly doesn't exist + return (enkelvoudiginformatieobject: null, status: CommandStatus.NotFound); + } + + // Throw the exception again so Polly knows a retry is needed (giving up after maximum reached retries) + throw new ConcurrencyConflictException("Concurrency conflict detected.", request.Id); + } + else + { + return (enkelvoudiginformatieobject: _enkelvoudigInformatieObject, status: CommandStatus.OK); + } + }, + cancellationToken + ); + + if (status == CommandStatus.NotFound) + { + return new CommandResult(null, CommandStatus.NotFound); + } + + if (status == CommandStatus.Conflict) + { + // Object exists but is locked by another process + var error = new ValidationError( + "nonFieldErrors", + ErrorCode.Conflict, + $"Het enkelvoudiginformatieobject {request.Id} is vergrendeld door een andere bewerking." + ); + return new CommandResult(null, CommandStatus.Conflict, error); + } + + using (var audittrail = _auditTrailFactory.Create(AuditTrailOptions, enkelvoudigInformatieObject.LegacyAuditTrail)) + { + audittrail.SetOld(enkelvoudigInformatieObject); + + if (request.Set) + { + if (enkelvoudigInformatieObject.Locked) + { + var error = new ValidationError("nonFieldErrors", ErrorCode.ExistingLock, "Het document is al gelockt."); + return new CommandResult(null, CommandStatus.ValidationError, error); + } + + enkelvoudigInformatieObject.Locked = true; + enkelvoudigInformatieObject.Lock = Guid.NewGuid().ToString().Replace("-", ""); + } + else + { + if (request.Lock != null && enkelvoudigInformatieObject.Lock != request.Lock) + { + var error = new ValidationError("nonFieldErrors", ErrorCode.IncorrectLockId, "Incorrect lock ID."); + return new CommandResult(null, CommandStatus.ValidationError, error); + } + + if (request.Lock == null) + { + if (!AuthorizationContextAccessor.AuthorizationContext.IsForcedUnlockAuthorized()) + { + var error = new ValidationError("nonFieldErrors", ErrorCode.MissingLockId, "Dit is een verplicht veld."); + return new CommandResult(null, CommandStatus.ValidationError, error); + } + } + + // Handle incompleted multi-part documents here if any + + var current = enkelvoudigInformatieObject.LatestEnkelvoudigInformatieObjectVersie; + if (current.Inhoud == null && current.Bestandsomvang > 0) + { + await AbortCurrentBestandsdelenUploadAsync(request, current, cancellationToken); + } + + enkelvoudigInformatieObject.Locked = false; + enkelvoudigInformatieObject.Lock = null; + } + + audittrail.SetNew(enkelvoudigInformatieObject); + + await audittrail.PatchedAsync(enkelvoudigInformatieObject, enkelvoudigInformatieObject, cancellationToken); + + await _context.SaveChangesAsync(cancellationToken); + + await tx.CommitAsync(cancellationToken); + + if (request.Set) + _logger.LogDebug( + "EnkelvoudigInformatieObject successfully locked. Lock={enkelvoudigInformatieObjectLock}", + enkelvoudigInformatieObject.Lock + ); + else + _logger.LogDebug("EnkelvoudigInformatieObject successfully unlocked."); + } + return new CommandResult(enkelvoudigInformatieObject.Lock, CommandStatus.OK); + } + + private async Task AbortCurrentBestandsdelenUploadAsync( + LockEnkelvoudigInformatieObjectCommand request, + EnkelvoudigInformatieObjectVersie current, + CancellationToken cancellationToken + ) + { + IMultiPartDocument multipartdocument = new MultiPartDocument(current.MultiPartDocumentId); + + // 1. Cleanup underlying (temporary files) from document-storage of the unmerged document + await AbortMultiPartUploadsFromDocumentStorage(multipartdocument, cancellationToken); + + // 2. Cleanup bestandsdelen en reset document versie meta-data + var bestandsdelen = await _context + .BestandsDelen.Where(e => e.EnkelvoudigInformatieObjectVersieId == current.Id) + .ToArrayAsync(cancellationToken); + + _context.BestandsDelen.RemoveRange(bestandsdelen); + + // 3. Reset document versie meta-data (inhoud, bestandsomvang, multipart id) so it becomes a meta-only document + current.Inhoud = null; + current.Bestandsomvang = 0; + current.MultiPartDocumentId = null; + current.BestandsDelen.Clear(); // Note: Important here to keep audittrail consistent with the actual locked/unlocked state of the document (see handling of incompleted multi-part documents in the code below) + + // Note: After SaveChangesAsync the current version (incompleted multi-part document) has become a meta-only document!! + } + + private async Task AbortMultiPartUploadsFromDocumentStorage(IMultiPartDocument multipartdocument, CancellationToken cancellationToken) + { + try + { + await _documentService.AbortMultipartUploadAsync(multipartdocument, cancellationToken); + } + catch (Exception ex) + { + // Only log the error so the Unlock can continue without any problems! + _logger.LogError(ex, "Could not abort current pending multi-part upload"); + } + } + + private static AuditTrailOptions AuditTrailOptions => + new AuditTrailOptions { Bron = ServiceRoleName.DRC, Resource = "enkelvoudiginformatieobject" }; +} + +public class LockEnkelvoudigInformatieObjectCommand : IRequest> +{ + public bool Set { get; internal set; } + public Guid Id { get; internal set; } + public string Lock { get; internal set; } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/UpdateEnkelvoudigInformatieObjectCommandHandler.cs b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/UpdateEnkelvoudigInformatieObjectCommandHandler.cs new file mode 100644 index 00000000..0edb223b --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Handlers/v1/7/UpdateEnkelvoudigInformatieObjectCommandHandler.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OneGround.ZGW.Catalogi.ServiceAgent.v1; +using OneGround.ZGW.Common.Contracts; +using OneGround.ZGW.Common.Contracts.v1; +using OneGround.ZGW.Common.Handlers; +using OneGround.ZGW.Common.Web.Authorization; +using OneGround.ZGW.Common.Web.Services; +using OneGround.ZGW.Common.Web.Services.AuditTrail; +using OneGround.ZGW.Common.Web.Services.UriServices; +using OneGround.ZGW.DataAccess; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; +using OneGround.ZGW.Documenten.Contracts.v1._7.Responses; +using OneGround.ZGW.Documenten.DataModel; +using OneGround.ZGW.Documenten.Services; +using OneGround.ZGW.Documenten.Web.Authorization; +using OneGround.ZGW.Documenten.Web.BusinessRules.v1; +using OneGround.ZGW.Documenten.Web.Concurrency; +using OneGround.ZGW.Documenten.Web.Extensions; +using OneGround.ZGW.Documenten.Web.Notificaties; +using OneGround.ZGW.Documenten.Web.Services; + +namespace OneGround.ZGW.Documenten.Web.Handlers.v1._7; + +public class UpdateEnkelvoudigInformatieObjectCommandHandler + : _5.MutatieEnkelvoudigInformatieObjectCommandHandler, + IRequestHandler> +{ + private readonly IEnkelvoudigInformatieObjectMerger _entityMerger; + private readonly ResilienceConcurrencyRetryPipeline _concurrencyRetryPipeline; + + public UpdateEnkelvoudigInformatieObjectCommandHandler( + ILogger logger, + IConfiguration configuration, + DrcDbContext context, + IEntityUriService uriService, + INummerGenerator nummerGenerator, + IDocumentServicesResolver documentServicesResolver, + IEnkelvoudigInformatieObjectBusinessRuleService enkelvoudigInformatieObjectBusinessRuleService, + ICatalogiServiceAgent catalogiServiceAgent, + IAuditTrailFactory auditTrailFactory, + IAuthorizationContextAccessor authorizationContextAccessor, + ILockGenerator lockGenerator, + IOptions formOptions, + INotificatieService notificatieService, + IDocumentKenmerkenResolver documentKenmerkenResolver, + IEnkelvoudigInformatieObjectMergerFactory entityMergerFactory, + ResilienceConcurrencyRetryPipeline concurrencyRetryPipeline + ) + : base( + logger, + configuration, + uriService, + authorizationContextAccessor, + documentServicesResolver, + context, + enkelvoudigInformatieObjectBusinessRuleService, + nummerGenerator, + catalogiServiceAgent, + auditTrailFactory, + lockGenerator, + formOptions, + notificatieService, + documentKenmerkenResolver + ) + { + _entityMerger = entityMergerFactory.Create(); + _concurrencyRetryPipeline = concurrencyRetryPipeline; + } + + public async Task> Handle( + UpdateEnkelvoudigInformatieObjectCommand request, + CancellationToken cancellationToken + ) + { + _logger.LogDebug("Updating (creating new version of existing) EnkelvoudigInformatieObject...."); + + var errors = new List(); + + // Use ReadCommitted isolation level: + // - FOR UPDATE provides pessimistic row-level locking (prevents concurrent modifications) + // - xmin (configured in EnkelvoudigInformatieObject) provides optimistic concurrency detection (detects any changes since read) + // - ReadCommitted allows better concurrency than Serializable for this use case + // - The combination prevents both lost updates (via FOR UPDATE) and write skew (via xmin) + using var tx = await _context.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); + + var rsinFilter = GetRsinFilterPredicate(); + + var (existingEnkelvoudigInformatieObject, status) = await _concurrencyRetryPipeline.ExecuteWithResultAsync( + async (token) => + { + // First, try to acquire lock on the EnkelvoudigInformatieObject + var _existingEnkelvoudigInformatieObject = await _context + .EnkelvoudigInformatieObjecten.LockForUpdate(_context, c => c.Id, [request.ExistingEnkelvoudigInformatieObjectId]) + .Where(rsinFilter) + .Include(e => e.LatestEnkelvoudigInformatieObjectVersie) + .SingleOrDefaultAsync(e => e.Id == request.ExistingEnkelvoudigInformatieObjectId, token); + + // The object might be locked OR not exist - check if it exists without lock + if (_existingEnkelvoudigInformatieObject == null) + { + // The object might be locked OR not exist - check if it exists without lock + var exists = await _context + .EnkelvoudigInformatieObjecten.Where(rsinFilter) + .AnyAsync(e => e.Id == request.ExistingEnkelvoudigInformatieObjectId, token); + + if (!exists) + { + // Object truly doesn't exist + return (enkelvoudiginformatieobject: null, status: CommandStatus.NotFound); + } + + // Throw the exception again so Polly knows a retry is needed (giving up after maximum reached retries) + throw new ConcurrencyConflictException("Concurrency conflict detected.", request.ExistingEnkelvoudigInformatieObjectId); + } + else + { + return (enkelvoudiginformatieobject: _existingEnkelvoudigInformatieObject, status: CommandStatus.OK); + } + }, + cancellationToken + ); + + if (status == CommandStatus.NotFound) + { + return new CommandResult(null, CommandStatus.NotFound); + } + + if (status == CommandStatus.Conflict) + { + // Object exists but is locked by another process + var error = new ValidationError( + "nonFieldErrors", + ErrorCode.Conflict, + $"Het enkelvoudiginformatieobject {request.ExistingEnkelvoudigInformatieObjectId} is vergrendeld door een andere bewerking." + ); + return new CommandResult(null, CommandStatus.Conflict, error); + } + + bool isPartialUpdate = request.PartialObject != null && request.EnkelvoudigInformatieObjectVersie == null; + + EnkelvoudigInformatieObjectVersie versie; + if (isPartialUpdate) + { + // Partial update (e.g. for PATCH endpoint) so merge the partial object provided by the client with the existing entity + versie = _entityMerger.TryMergeWithPartial(request.PartialObject, existingEnkelvoudigInformatieObject, errors); + if (errors.Count != 0) + { + return new CommandResult(null, CommandStatus.ValidationError, errors.ToArray()); + } + } + else + { + // Full update (e.g. for PUT endpoint) with EnkelvoudigInformatieObjectVersie provided by the client + versie = request.EnkelvoudigInformatieObjectVersie; + } + + if (!_authorizationContext.IsAuthorized(existingEnkelvoudigInformatieObject, AuthorizationScopes.Documenten.Update)) + { + return new CommandResult(null, CommandStatus.Forbidden); + } + + var currentVersie = existingEnkelvoudigInformatieObject.LatestEnkelvoudigInformatieObjectVersie; + + versie.Versie = currentVersie.Versie + 1; + versie.Owner = currentVersie.InformatieObject.Owner; + + versie.SetLinkToNullWhenInvalid(); + versie.EscapeBestandsNaamWhenInvalid(); + + await _enkelvoudigInformatieObjectBusinessRuleService.ValidateAsync( + versie, + _applicationConfiguration.IgnoreInformatieObjectTypeValidation, + request.ExistingEnkelvoudigInformatieObjectId, + isPartialUpdate, + apiVersie: 1.7M, + errors, + cancellationToken + ); + + if (errors.Count != 0) + { + return new CommandResult(null, CommandStatus.ValidationError, errors.ToArray()); + } + + // Note: Vertrouwelijkheidaanduiding van een informatieobject (drc-007) => get from request or get from Catalogi.InformatieObjectType + await SetVertrouwelijkheidAanduidingAsync(versie); + + using (var audittrail = _auditTrailFactory.Create(AuditTrailOptions, currentVersie.LatestInformatieObject.LegacyAuditTrail)) + { + // Add a new version of the existing EnkelvoudigInformatieObject + audittrail.SetOld(existingEnkelvoudigInformatieObject); + + var informatieObjectType = versie.InformatieObject.InformatieObjectType; + + var indicatieGebruiksrecht = versie.InformatieObject.IndicatieGebruiksrecht; + + versie.BeginRegistratie = DateTime.UtcNow; + versie.EnkelvoudigInformatieObjectId = request.ExistingEnkelvoudigInformatieObjectId; + // Clone the EnkelvoudigInformatieObject from previous version + versie.InformatieObject = existingEnkelvoudigInformatieObject; + versie.InformatieObject.InformatieObjectType = informatieObjectType; + versie.InformatieObject.IndicatieGebruiksrecht = indicatieGebruiksrecht; + + // Depending on the specified inhoud and bestandsomvang verschillende manieren om documenten toe te voegen.... + if (IsDocumentUploadWithBestandsdelen(versie.Bestandsomvang, versie.Inhoud)) + { + // We have enabled (some) metadata fields for the underlying document provider + var metadata = new DocumentMeta { Rsin = versie.InformatieObject.Owner, Version = versie.Versie }; + + var multipartUploadResult = await DocumentService.InitiateMultipartUploadAsync(versie.Bestandsnaam, metadata, cancellationToken); + + versie.MultiPartDocumentId = multipartUploadResult.Context; + + AddBestandsDelenToEnkelvoudigeInformatieObjectVersie(versie); + + versie.Inhoud = null; + } + else if (IsDocumentMetaOnly(versie.Bestandsomvang, versie.Inhoud)) + { + versie.Inhoud = null; + } + else if (!versie.Inhoud.IsAnyDocumentUrn()) // New base64 encoded Inhoud specified (in request) or existing document-urn from merge operation? + { + var (inhoud, bestandsomvang) = await TryAddDocumentToDocumentStore(versie, errors, cancellationToken); + if (errors.Count != 0) + { + return new CommandResult(null, CommandStatus.ValidationError, errors.ToArray()); + } + versie.Inhoud = inhoud; + versie.Bestandsomvang = bestandsomvang; + } + + // Use external identificatie if specified generate otherwise + if (string.IsNullOrEmpty(versie.Identificatie)) + { + var owner = versie.Owner; + + var enkelvoudigInformatieObjectNummer = await _nummerGenerator.GenerateAsync( + owner, + "documenten", + id => IsEnkelvoudigInformatieObjectVersieUnique(owner, id), + cancellationToken + ); + + versie.Identificatie = enkelvoudigInformatieObjectNummer; + } + + await _context.EnkelvoudigInformatieObjectVersies.AddAsync(versie, cancellationToken); // Note: Sequential Guid for Id is generated here by the DBMS + + versie.InformatieObject.LatestEnkelvoudigInformatieObjectVersieId = versie.Id; + versie.InformatieObject.LatestEnkelvoudigInformatieObjectVersie = versie; + versie.InformatieObject.LatestVertrouwelijkheidAanduiding = versie.Vertrouwelijkheidaanduiding; + versie.LatestInformatieObject = versie.InformatieObject; + + audittrail.SetNew(versie.InformatieObject); + + if (isPartialUpdate) + { + await audittrail.PatchedAsync(versie.InformatieObject, versie.InformatieObject, cancellationToken); + } + else + { + await audittrail.UpdatedAsync(versie.InformatieObject, versie.InformatieObject, cancellationToken); + } + + await _context.SaveChangesAsync(cancellationToken); + + await tx.CommitAsync(cancellationToken); + } + + _logger.LogDebug("EnkelvoudigInformatieObject {Id} successfully created new version", versie.InformatieObject.Id); + + // Note: Do not send notification using bestandsdelen (this is done when all bestandsdelen are uploaded an checkin is called) + if (versie.BestandsDelen.Count == 0) + { + await SendNotificationAsync(Actie.update, versie.InformatieObject, cancellationToken); + } + + return new CommandResult(versie, CommandStatus.OK); + } +} + +public class UpdateEnkelvoudigInformatieObjectCommand : IRequest> +{ + public Guid ExistingEnkelvoudigInformatieObjectId { get; internal set; } + public EnkelvoudigInformatieObjectVersie EnkelvoudigInformatieObjectVersie { get; internal set; } // For PUT endpoint, contains the full update sent by the client + public dynamic PartialObject { get; internal set; } // For PATCH endpoint, contains the partial update sent by the client +} diff --git a/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/DomainToResponseProfile.cs b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/DomainToResponseProfile.cs new file mode 100644 index 00000000..a8b77243 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/DomainToResponseProfile.cs @@ -0,0 +1,114 @@ +using AutoMapper; +using OneGround.ZGW.Common.Helpers; +using OneGround.ZGW.Common.Web.Mapping.ValueResolvers; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; +using OneGround.ZGW.Documenten.Contracts.v1._7.Responses; +using OneGround.ZGW.Documenten.DataModel; + +namespace OneGround.ZGW.Documenten.Web.MappingProfiles.v1._7; + +public class DomainToResponseProfile : Profile +{ + public DomainToResponseProfile() + { + CreateMap() + .ForMember(dest => dest.Url, opt => opt.MapFrom()) + .ForMember(dest => dest.Versie, opt => opt.Ignore()) + .ForMember(dest => dest.BeginRegistratie, opt => opt.Ignore()) + .ForMember(dest => dest.Bestandsomvang, opt => opt.Ignore()) + .ForMember(dest => dest.Identificatie, opt => opt.Ignore()) + .ForMember(dest => dest.Bronorganisatie, opt => opt.Ignore()) + .ForMember(dest => dest.CreatieDatum, opt => opt.Ignore()) + .ForMember(dest => dest.Titel, opt => opt.Ignore()) + .ForMember(dest => dest.Vertrouwelijkheidaanduiding, opt => opt.Ignore()) + .ForMember(dest => dest.IsGereedVoorPublicatie, opt => opt.Ignore()) + .ForMember(dest => dest.TonenAanInitiator, opt => opt.Ignore()) + .ForMember(dest => dest.Auteur, opt => opt.Ignore()) + .ForMember(dest => dest.Status, opt => opt.Ignore()) + .ForMember(dest => dest.Formaat, opt => opt.Ignore()) + .ForMember(dest => dest.Taal, opt => opt.Ignore()) + .ForMember(dest => dest.Bestandsnaam, opt => opt.Ignore()) + .ForMember(dest => dest.Inhoud, opt => opt.Ignore()) + .ForMember(dest => dest.Link, opt => opt.Ignore()) + .ForMember(dest => dest.Beschrijving, opt => opt.Ignore()) + .ForMember(dest => dest.OntvangstDatum, opt => opt.Ignore()) + .ForMember(dest => dest.VerzendDatum, opt => opt.Ignore()) + .ForMember(dest => dest.IndicatieGebruiksrecht, opt => opt.Ignore()) + .ForMember(dest => dest.Ondertekening, opt => opt.Ignore()) + .ForMember(dest => dest.Integriteit, opt => opt.Ignore()) + .ForMember(dest => dest.Verschijningsvorm, opt => opt.Ignore()) + .ForMember(dest => dest.Trefwoorden, opt => opt.Ignore()) + .AfterMap(); + + CreateMap() + .ForMember(dest => dest.Url, opt => opt.MapFrom(src => src.InformatieObject)) + .ForMember(dest => dest.CreatieDatum, opt => opt.MapFrom(src => ProfileHelper.StringDateFromDate(src.CreatieDatum))) + .ForMember(dest => dest.OntvangstDatum, opt => opt.MapFrom(src => ProfileHelper.StringDateFromDate(src.OntvangstDatum))) + .ForMember(dest => dest.BeginRegistratie, opt => opt.MapFrom(src => ProfileHelper.StringDateFromDateTime(src.BeginRegistratie, true))) + .ForMember(dest => dest.VerzendDatum, opt => opt.MapFrom(src => ProfileHelper.StringDateFromDate(src.VerzendDatum))) + .ForMember( + dest => dest.Ondertekening, + opt => opt.MapFrom(src => EnkelvoudigInformatieObjectVersieMapperHelper.CreateOptionalOndertekeningDto(src, true)) + ) + .ForMember( + dest => dest.Integriteit, + opt => opt.MapFrom(src => EnkelvoudigInformatieObjectVersieMapperHelper.CreateOptionalIntegriteitDto(src, true)) + ) + .ForMember(dest => dest.IndicatieGebruiksrecht, opt => opt.MapFrom(src => src.InformatieObject.IndicatieGebruiksrecht)) + .ForMember(dest => dest.Locked, opt => opt.MapFrom(src => src.InformatieObject.Locked)) + .ForMember(dest => dest.Lock, opt => opt.MapFrom(src => src.InformatieObject.Lock)) + .ForMember(dest => dest.Verschijningsvorm, opt => opt.MapFrom(src => src.Verschijningsvorm)) + .ForMember(dest => dest.Trefwoorden, opt => opt.MapFrom(src => src.Trefwoorden)) + .ForMember(dest => dest.Inhoud, opt => opt.Ignore()) + .ForMember(dest => dest.InformatieObjectType, opt => opt.MapFrom(src => src.InformatieObject.InformatieObjectType)) + .AfterMap(); + + CreateMap() + .ForMember(dest => dest.Url, opt => opt.MapFrom(src => src.InformatieObject)) + .ForMember(dest => dest.CreatieDatum, opt => opt.MapFrom(src => ProfileHelper.StringDateFromDate(src.CreatieDatum))) + .ForMember(dest => dest.OntvangstDatum, opt => opt.MapFrom(src => ProfileHelper.StringDateFromDate(src.OntvangstDatum))) + .ForMember(dest => dest.BeginRegistratie, opt => opt.MapFrom(src => ProfileHelper.StringDateFromDateTime(src.BeginRegistratie, true))) + .ForMember(dest => dest.VerzendDatum, opt => opt.MapFrom(src => ProfileHelper.StringDateFromDate(src.VerzendDatum))) + .ForMember( + dest => dest.Ondertekening, + opt => opt.MapFrom(src => EnkelvoudigInformatieObjectVersieMapperHelper.CreateOptionalOndertekeningDto(src, true)) + ) + .ForMember( + dest => dest.Integriteit, + opt => opt.MapFrom(src => EnkelvoudigInformatieObjectVersieMapperHelper.CreateOptionalIntegriteitDto(src, true)) + ) + .ForMember(dest => dest.IndicatieGebruiksrecht, opt => opt.MapFrom(src => src.InformatieObject.IndicatieGebruiksrecht)) + .ForMember(dest => dest.Locked, opt => opt.MapFrom(src => src.InformatieObject.Locked)) + .ForMember(dest => dest.Lock, opt => opt.MapFrom(src => src.InformatieObject.Lock)) + .ForMember(dest => dest.Verschijningsvorm, opt => opt.MapFrom(src => src.Verschijningsvorm)) + .ForMember(dest => dest.Trefwoorden, opt => opt.MapFrom(src => src.Trefwoorden)) + .ForMember(dest => dest.Inhoud, opt => opt.Ignore()) + .ForMember(dest => dest.InformatieObjectType, opt => opt.MapFrom(src => src.InformatieObject.InformatieObjectType)) + .AfterMap(); + + // Note: This map is used to merge an existing ENKELVOUDIGINFORMATIEOBJECT(+VERSIE) with the PATCH operation + CreateMap() + .ForMember(dest => dest.Identificatie, opt => opt.Ignore()) + .ForMember(dest => dest.Bronorganisatie, opt => opt.Ignore()) + .ForMember(dest => dest.CreatieDatum, opt => opt.Ignore()) + .ForMember(dest => dest.Titel, opt => opt.Ignore()) + .ForMember(dest => dest.Vertrouwelijkheidaanduiding, opt => opt.Ignore()) + .ForMember(dest => dest.IsGereedVoorPublicatie, opt => opt.Ignore()) + .ForMember(dest => dest.TonenAanInitiator, opt => opt.Ignore()) + .ForMember(dest => dest.Auteur, opt => opt.Ignore()) + .ForMember(dest => dest.Status, opt => opt.Ignore()) + .ForMember(dest => dest.Formaat, opt => opt.Ignore()) + .ForMember(dest => dest.Taal, opt => opt.Ignore()) + .ForMember(dest => dest.Bestandsnaam, opt => opt.Ignore()) + .ForMember(dest => dest.Inhoud, opt => opt.Ignore()) + .ForMember(dest => dest.Link, opt => opt.Ignore()) + .ForMember(dest => dest.Beschrijving, opt => opt.Ignore()) + .ForMember(dest => dest.OntvangstDatum, opt => opt.Ignore()) + .ForMember(dest => dest.VerzendDatum, opt => opt.Ignore()) + .ForMember(dest => dest.IndicatieGebruiksrecht, opt => opt.Ignore()) + .ForMember(dest => dest.Ondertekening, opt => opt.Ignore()) + .ForMember(dest => dest.Integriteit, opt => opt.Ignore()) + .ForMember(dest => dest.Lock, opt => opt.Ignore()) // Note: Don't merge the lock value because we have to validate the value from request and not the one in the database after the merge) + .AfterMap(); + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapDownloadLink.cs b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapDownloadLink.cs new file mode 100644 index 00000000..b4304698 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapDownloadLink.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using OneGround.ZGW.Common.Web.Services.UriServices; +using OneGround.ZGW.Documenten.Contracts.v1._7.Responses; +using OneGround.ZGW.Documenten.DataModel; + +namespace OneGround.ZGW.Documenten.Web.MappingProfiles.v1._7; + +public class MapDownloadLink : IMappingAction +{ + private readonly IEntityUriService _uriService; + + public MapDownloadLink(IEntityUriService uriService) + { + _uriService = uriService; + } + + public void Process(EnkelvoudigInformatieObjectVersie src, EnkelvoudigInformatieObjectResponseDto dest, ResolutionContext context) + { + if ((string.IsNullOrEmpty(src.Inhoud) && src.Bestandsomvang == 0) || src.BestandsDelen.Count != 0) // Note: New in v1.1 + dest.Inhoud = null; + else + dest.Inhoud = _uriService.GetUri(src); + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapLatestEnkelvoudigInformatieObjectVersieRequest.cs b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapLatestEnkelvoudigInformatieObjectVersieRequest.cs new file mode 100644 index 00000000..be737e28 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapLatestEnkelvoudigInformatieObjectVersieRequest.cs @@ -0,0 +1,46 @@ +using AutoMapper; +using OneGround.ZGW.Common.Helpers; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; +using OneGround.ZGW.Documenten.DataModel; + +namespace OneGround.ZGW.Documenten.Web.MappingProfiles.v1._7; + +public class MapLatestEnkelvoudigInformatieObjectVersieRequest + : IMappingAction +{ + public void Process(EnkelvoudigInformatieObject src, EnkelvoudigInformatieObjectUpdateRequestDto dest, ResolutionContext context) + { + // Note: For update-request-mapping we get always get the latest version + var latestVersion = src.LatestEnkelvoudigInformatieObjectVersie; + + dest.Bronorganisatie = latestVersion.Bronorganisatie; + dest.Identificatie = latestVersion.Identificatie; + dest.CreatieDatum = ProfileHelper.StringDateFromDate(latestVersion.CreatieDatum); + dest.Titel = latestVersion.Titel; + dest.Vertrouwelijkheidaanduiding = $"{latestVersion.Vertrouwelijkheidaanduiding}"; + dest.IsGereedVoorPublicatie = latestVersion.IsGereedVoorPublicatie; + dest.TonenAanInitiator = latestVersion.TonenAanInitiator; + dest.Auteur = latestVersion.Auteur; + dest.Status = $"{latestVersion.Status}"; + dest.Formaat = latestVersion.Formaat; + dest.Taal = latestVersion.Taal; + dest.Bestandsnaam = latestVersion.Bestandsnaam; + dest.Bestandsomvang = latestVersion.Bestandsomvang; + dest.Inhoud = latestVersion.Inhoud; + dest.Link = latestVersion.Link; + dest.Beschrijving = latestVersion.Beschrijving; + dest.OntvangstDatum = ProfileHelper.StringDateFromDate(latestVersion.OntvangstDatum); + dest.VerzendDatum = ProfileHelper.StringDateFromDate(latestVersion.VerzendDatum); + dest.Ondertekening = EnkelvoudigInformatieObjectVersieMapperHelper.CreateOptionalOndertekeningDto(latestVersion, false); + dest.Integriteit = EnkelvoudigInformatieObjectVersieMapperHelper.CreateOptionalIntegriteitDto(latestVersion, false); + dest.InformatieObjectType = latestVersion.LatestInformatieObject.InformatieObjectType; + dest.IndicatieGebruiksrecht = latestVersion.LatestInformatieObject.IndicatieGebruiksrecht; + + dest.Verschijningsvorm = latestVersion.Verschijningsvorm; + dest.Trefwoorden = latestVersion.Trefwoorden; + dest.InhoudIsVervallen = latestVersion.InhoudIsVervallen; + + // Note: Don't merge the lock value because we have to validate the value from request and not the one in the database after the merge) + // (meaning don't: dest.Lock = latestVersion.LatestInformatieObject.Lock) + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapLatestEnkelvoudigInformatieObjectVersieResponse.cs b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapLatestEnkelvoudigInformatieObjectVersieResponse.cs new file mode 100644 index 00000000..c7dee6d3 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/MapLatestEnkelvoudigInformatieObjectVersieResponse.cs @@ -0,0 +1,73 @@ +using System.Linq; +using AutoMapper; +using OneGround.ZGW.Common.Helpers; +using OneGround.ZGW.Common.Web.Services.UriServices; +using OneGround.ZGW.Documenten.Contracts.v1._7.Responses; +using OneGround.ZGW.Documenten.DataModel; + +namespace OneGround.ZGW.Documenten.Web.MappingProfiles.v1._7; + +public class MapLatestEnkelvoudigInformatieObjectVersieResponse + : IMappingAction +{ + private readonly IEntityUriService _uriService; + + public MapLatestEnkelvoudigInformatieObjectVersieResponse(IEntityUriService uriService) + { + _uriService = uriService; + } + + public void Process(EnkelvoudigInformatieObject src, EnkelvoudigInformatieObjectGetResponseDto dest, ResolutionContext context) + { + // Note: For update-request-mapping we get always get the latest version + var latestVersion = src.LatestEnkelvoudigInformatieObjectVersie; + + dest.Versie = latestVersion.Versie; + dest.Bronorganisatie = latestVersion.Bronorganisatie; + dest.Identificatie = latestVersion.Identificatie; + dest.Bestandsomvang = latestVersion.Bestandsomvang; + dest.BeginRegistratie = ProfileHelper.StringDateFromDateTime(latestVersion.BeginRegistratie, withTime: true); + dest.CreatieDatum = ProfileHelper.StringDateFromDate(latestVersion.CreatieDatum); + dest.Titel = latestVersion.Titel; + dest.Vertrouwelijkheidaanduiding = $"{latestVersion.Vertrouwelijkheidaanduiding}"; + dest.IsGereedVoorPublicatie = latestVersion.IsGereedVoorPublicatie; + dest.TonenAanInitiator = latestVersion.TonenAanInitiator; + dest.Auteur = latestVersion.Auteur; + dest.Status = $"{latestVersion.Status}"; + dest.Formaat = latestVersion.Formaat; + dest.Taal = latestVersion.Taal; + dest.Bestandsnaam = latestVersion.Bestandsnaam; + dest.Bestandsomvang = latestVersion.Bestandsomvang; + dest.Link = latestVersion.Link; + dest.Inhoud = latestVersion.BestandsDelen.Count != 0 ? null : _uriService.GetUri(latestVersion); + dest.Beschrijving = latestVersion.Beschrijving; + dest.OntvangstDatum = ProfileHelper.StringDateFromDate(latestVersion.OntvangstDatum); + dest.VerzendDatum = ProfileHelper.StringDateFromDate(latestVersion.VerzendDatum); + dest.Ondertekening = EnkelvoudigInformatieObjectVersieMapperHelper.CreateOptionalOndertekeningDto(latestVersion, true); + dest.Integriteit = EnkelvoudigInformatieObjectVersieMapperHelper.CreateOptionalIntegriteitDto(latestVersion, true); + + dest.InformatieObjectType = latestVersion.LatestInformatieObject.InformatieObjectType; + dest.IndicatieGebruiksrecht = latestVersion.LatestInformatieObject.IndicatieGebruiksrecht; + dest.Locked = latestVersion.LatestInformatieObject.Locked; + + dest.Verschijningsvorm = latestVersion.Verschijningsvorm; + dest.Trefwoorden = latestVersion.Trefwoorden; + dest.InhoudIsVervallen = latestVersion.InhoudIsVervallen; + + dest.BestandsDelen = latestVersion.BestandsDelen.OrderBy(d => d.Volgnummer).Select(MapBestandsDeel).ToList(); + } + + private Documenten.Contracts.v1._1.Responses.BestandsDeelResponseDto MapBestandsDeel(BestandsDeel bestandsdeel) + { + var uri = _uriService.GetUri(bestandsdeel); + + return new Documenten.Contracts.v1._1.Responses.BestandsDeelResponseDto + { + Url = uri, + Omvang = bestandsdeel.Omvang, + Volgnummer = bestandsdeel.Volgnummer, + Voltooid = bestandsdeel.Voltooid, + Lock = bestandsdeel.EnkelvoudigInformatieObjectVersie.LatestInformatieObject.Lock, + }; + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/RequestToDomainProfile.cs b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/RequestToDomainProfile.cs new file mode 100644 index 00000000..680a3054 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/7/RequestToDomainProfile.cs @@ -0,0 +1,260 @@ +using System; +using AutoMapper; +using OneGround.ZGW.Common.DataModel; +using OneGround.ZGW.Common.Helpers; +using OneGround.ZGW.Documenten.Contracts.v1._7.Queries; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; +using OneGround.ZGW.Documenten.DataModel; +using OneGround.ZGW.Documenten.Web.Models.v1._7; + +namespace OneGround.ZGW.Documenten.Web.MappingProfiles.v1._7; + +// Note: This Profile adds extended mappings (above the ones defined in v1.0, v1.1 and 1.5) +public class RequestToDomainProfile : Profile +{ + public RequestToDomainProfile() + { + CreateMap() + .ForMember(dest => dest.Trefwoorden_In, opt => opt.MapFrom(src => ProfileHelper.ArrayFromString(src.Trefwoorden))) + .AfterMap( + (src, dest) => + { + if (src.Trefwoorden == null) + dest.Trefwoorden_In = null; + } + ); // Note: Don't map to an empty list!! (EF Where Query on NULL) + + // for enkelvoudiginformatieobject search endpoint + CreateMap() + .ForMember(dest => dest.Trefwoorden_In, opt => opt.MapFrom(src => ProfileHelper.ArrayFromString(src.Trefwoorden))) + .AfterMap( + (src, dest) => + { + if (src.Trefwoorden == null) + dest.Trefwoorden_In = null; + + if (src.Uuid_In == null) + dest.Uuid_In = null; + } + ); // Note: Don't map to an empty list!! (EF Where Query on NULL) + + // Create new initial EnkelvoudigInformatieObject: versie 1 + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) + .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()) + .ForMember(dest => dest.CreationTime, opt => opt.Ignore()) + .ForMember(dest => dest.CreationTime, opt => opt.Ignore()) + .ForMember(dest => dest.ModificationTime, opt => opt.Ignore()) + .ForMember(dest => dest.Locked, opt => opt.Ignore()) + .ForMember(dest => dest.Lock, opt => opt.Ignore()) + .ForMember(dest => dest.ObjectInformatieObjecten, opt => opt.Ignore()) + .ForMember(dest => dest.GebruiksRechten, opt => opt.Ignore()) + .ForMember(dest => dest.EnkelvoudigInformatieObjectVersies, opt => opt.Ignore()) + .ForMember(dest => dest.LatestEnkelvoudigInformatieObjectVersieId, opt => opt.Ignore()) + .ForMember(dest => dest.LatestEnkelvoudigInformatieObjectVersie, opt => opt.Ignore()) + .ForMember(dest => dest.LatestVertrouwelijkheidAanduiding, opt => opt.Ignore()) + .ForMember(dest => dest.RowVersion, opt => opt.Ignore()); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.CreatieDatum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.CreatieDatum))) + .ForMember(dest => dest.OntvangstDatum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.OntvangstDatum))) + .ForMember(dest => dest.VerzendDatum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.VerzendDatum))) + .ForMember( + dest => dest.InformatieObject, + opt => + opt.MapFrom(src => new EnkelvoudigInformatieObject + { + InformatieObjectType = src.InformatieObjectType.TrimEnd('/'), + IndicatieGebruiksrecht = src.IndicatieGebruiksrecht, + }) + ) + .ForMember(dest => dest.Ondertekening_Datum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.Ondertekening.Datum))) + .ForMember(dest => dest.Ondertekening_Soort, opt => opt.MapFrom(src => SoortFromString(src.Ondertekening.Soort))) + .ForMember(dest => dest.Integriteit_Algoritme, opt => opt.MapFrom(src => AlgoritmeFromString(src.Integriteit.Algoritme))) + .ForMember(dest => dest.Integriteit_Datum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.Integriteit.Datum))) + .ForMember(dest => dest.Integriteit_Waarde, opt => opt.MapFrom(src => src.Integriteit.Waarde)) + .ForMember( + dest => dest.Vertrouwelijkheidaanduiding, + opt => opt.MapFrom(src => VertrouwelijkheidAanduidingFromString(src.Vertrouwelijkheidaanduiding)) + ) + .ForMember(dest => dest.Status, opt => opt.MapFrom(src => StatusFromString(src.Status))) + .ForMember(dest => dest.Integriteit_Algoritme, opt => opt.MapFrom(src => AlgoritmeFromString(src.Integriteit.Algoritme))) + .ForMember(dest => dest.Verschijningsvorm, opt => opt.MapFrom(src => src.Verschijningsvorm)) + .ForMember(dest => dest.Trefwoorden, opt => opt.MapFrom(src => src.Trefwoorden)) + .ForMember(dest => dest.Versie, opt => opt.Ignore()) + .ForMember( + dest => dest.Taal, + opt => opt.MapFrom(src => ProfileHelper.Convert2letterTo3Letter(src.Taal, ProfileHelper.Taal2letterTo3LetterMap)) + ) + .ForMember(dest => dest.BeginRegistratie, opt => opt.Ignore()) + .ForMember(dest => dest.Bestandsomvang, opt => opt.MapFrom(src => src.Bestandsomvang)) + .ForMember(dest => dest.EnkelvoudigInformatieObjectId, opt => opt.Ignore()) + .ForMember(dest => dest.LatestInformatieObject, opt => opt.Ignore()) + .ForMember(dest => dest.RowVersion, opt => opt.Ignore()); + + // Create new version of EnkelvoudigInformatieObject: versie 2, versie 3, etc + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) + .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()) + .ForMember(dest => dest.CreationTime, opt => opt.Ignore()) + .ForMember(dest => dest.ModificationTime, opt => opt.Ignore()) + .ForMember(dest => dest.Locked, opt => opt.Ignore()) + .ForMember(dest => dest.ObjectInformatieObjecten, opt => opt.Ignore()) + .ForMember(dest => dest.GebruiksRechten, opt => opt.Ignore()) + .ForMember(dest => dest.EnkelvoudigInformatieObjectVersies, opt => opt.Ignore()) + .ForMember(dest => dest.LatestEnkelvoudigInformatieObjectVersieId, opt => opt.Ignore()) + .ForMember(dest => dest.LatestEnkelvoudigInformatieObjectVersie, opt => opt.Ignore()) + .ForMember(dest => dest.LatestVertrouwelijkheidAanduiding, opt => opt.Ignore()) + .ForMember(dest => dest.RowVersion, opt => opt.Ignore()); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.CreatieDatum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.CreatieDatum))) + .ForMember(dest => dest.OntvangstDatum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.OntvangstDatum))) + .ForMember(dest => dest.VerzendDatum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.VerzendDatum))) + .ForMember( + dest => dest.InformatieObject, + opt => + opt.MapFrom(src => new EnkelvoudigInformatieObject + { + InformatieObjectType = src.InformatieObjectType, + Lock = src.Lock, + IndicatieGebruiksrecht = src.IndicatieGebruiksrecht, + }) + ) + .ForMember(dest => dest.Ondertekening_Datum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.Ondertekening.Datum))) + .ForMember(dest => dest.Ondertekening_Soort, opt => opt.MapFrom(src => SoortFromString(src.Ondertekening.Soort))) + .ForMember(dest => dest.Integriteit_Algoritme, opt => opt.MapFrom(src => AlgoritmeFromString(src.Integriteit.Algoritme))) + .ForMember(dest => dest.Integriteit_Datum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.Integriteit.Datum))) + .ForMember(dest => dest.Integriteit_Waarde, opt => opt.MapFrom(src => src.Integriteit.Waarde)) + .ForMember( + dest => dest.Vertrouwelijkheidaanduiding, + opt => opt.MapFrom(src => VertrouwelijkheidAanduidingFromString(src.Vertrouwelijkheidaanduiding)) + ) + .ForMember(dest => dest.Status, opt => opt.MapFrom(src => StatusFromString(src.Status))) + .ForMember(dest => dest.Integriteit_Algoritme, opt => opt.MapFrom(src => AlgoritmeFromString(src.Integriteit.Algoritme))) + .ForMember(dest => dest.Verschijningsvorm, opt => opt.MapFrom(src => src.Verschijningsvorm)) + .ForMember(dest => dest.Trefwoorden, opt => opt.MapFrom(src => src.Trefwoorden)) + .ForMember(dest => dest.Versie, opt => opt.Ignore()) + .ForMember( + dest => dest.Taal, + opt => opt.MapFrom(src => ProfileHelper.Convert2letterTo3Letter(src.Taal, ProfileHelper.Taal2letterTo3LetterMap)) + ) + .ForMember(dest => dest.BeginRegistratie, opt => opt.Ignore()) + .ForMember(dest => dest.Bestandsomvang, opt => opt.MapFrom(src => src.Bestandsomvang)) + .ForMember(dest => dest.EnkelvoudigInformatieObjectId, opt => opt.Ignore()) + .ForMember(dest => dest.LatestInformatieObject, opt => opt.Ignore()) + .ForMember(dest => dest.RowVersion, opt => opt.Ignore()); + + //CreateMap() + // .ForMember(dest => dest.Startdatum__gt, opt => opt.MapFrom(src => ProfileHelper.DateTimeFromString(src.Startdatum__gt))) + // .ForMember(dest => dest.Startdatum__gte, opt => opt.MapFrom(src => ProfileHelper.DateTimeFromString(src.Startdatum__gte))) + // .ForMember(dest => dest.Startdatum__lt, opt => opt.MapFrom(src => ProfileHelper.DateTimeFromString(src.Startdatum__lt))) + // .ForMember(dest => dest.Startdatum__lte, opt => opt.MapFrom(src => ProfileHelper.DateTimeFromString(src.Startdatum__lte))) + // .ForMember(dest => dest.Einddatum__gt, opt => opt.MapFrom(src => ProfileHelper.DateTimeFromString(src.Einddatum__gt))) + // .ForMember(dest => dest.Einddatum__gte, opt => opt.MapFrom(src => ProfileHelper.DateTimeFromString(src.Einddatum__gte))) + // .ForMember(dest => dest.Einddatum__lt, opt => opt.MapFrom(src => ProfileHelper.DateTimeFromString(src.Einddatum__lt))) + // .ForMember(dest => dest.Einddatum__lte, opt => opt.MapFrom(src => ProfileHelper.DateTimeFromString(src.Einddatum__lte))); + + //CreateMap(); + + //CreateMap() + // .ForMember(dest => dest.Huisletter, opt => opt.MapFrom(src => src.Huisletter)) + // .ForMember(dest => dest.Huisnummer, opt => opt.MapFrom(src => src.Huisnummer)) + // .ForMember(dest => dest.HuisnummerToevoeging, opt => opt.MapFrom(src => src.HuisnummerToevoeging)) + // .ForMember(dest => dest.NaamOpenbareRuimte, opt => opt.MapFrom(src => src.NaamOpenbareRuimte)) + // .ForMember(dest => dest.Postcode, opt => opt.MapFrom(src => src.Postcode)) + // .ForMember(dest => dest.WoonplaatsNaam, opt => opt.MapFrom(src => src.WoonplaatsNaam)); + + //CreateMap() + // .ForMember(dest => dest.AdresBuitenland1, opt => opt.MapFrom(src => src.AdresBuitenland1)) + // .ForMember(dest => dest.AdresBuitenland2, opt => opt.MapFrom(src => src.AdresBuitenland2)) + // .ForMember(dest => dest.AdresBuitenland3, opt => opt.MapFrom(src => src.AdresBuitenland3)) + // .ForMember(dest => dest.LandPostadres, opt => opt.MapFrom(src => src.LandPostadres)); + + //CreateMap() + // .ForMember(dest => dest.PostbusOfAntwoordnummer, opt => opt.MapFrom(src => src.PostbusOfAntwoordnummer)) + // .ForMember(dest => dest.PostadresPostcode, opt => opt.MapFrom(src => src.PostadresPostcode)) + // .ForMember(dest => dest.PostadresType, opt => opt.MapFrom(src => src.PostadresType)) + // .ForMember(dest => dest.WoonplaatsNaam, opt => opt.MapFrom(src => src.WoonplaatsNaam)); + + //CreateMap() + // .ForMember(dest => dest.Betrokkene, opt => opt.MapFrom(src => src.Betrokkene)) + // .ForMember(dest => dest.AardRelatie, opt => opt.MapFrom(src => AardRelatieFromString(src.AardRelatie))) + // .ForMember(dest => dest.Toelichting, opt => opt.MapFrom(src => src.Toelichting)) + // .ForMember(dest => dest.Ontvangstdatum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.OntvangstDatum))) + // .ForMember(dest => dest.Verzenddatum, opt => opt.MapFrom(src => ProfileHelper.DateFromStringOptional(src.Verzenddatum))) + // .ForMember(dest => dest.Contactpersoon, opt => opt.MapFrom(src => src.Contactpersoon)) + // .ForMember(dest => dest.BinnenlandsCorrespondentieAdres, opt => opt.MapFrom(src => src.BinnenlandsCorrespondentieAdres)) + // .ForMember(dest => dest.BuitenlandsCorrespondentieAdres, opt => opt.MapFrom(src => src.BuitenlandsCorrespondentieAdres)) + // .ForMember(dest => dest.CorrespondentiePostadres, opt => opt.MapFrom(src => src.CorrespondentiePostadres)) + // .ForMember(dest => dest.Faxnummer, opt => opt.MapFrom(src => src.Faxnummer)) + // .ForMember(dest => dest.EmailAdres, opt => opt.MapFrom(src => src.EmailAdres)) + // .ForMember(dest => dest.MijnOverheid, opt => opt.MapFrom(src => src.MijnOverheid)) + // .ForMember(dest => dest.Telefoonnummer, opt => opt.MapFrom(src => src.Telefoonnummer)) + // .ForMember(dest => dest.Id, opt => opt.Ignore()) + // .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) + // .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()) + // .ForMember(dest => dest.CreationTime, opt => opt.Ignore()) + // .ForMember(dest => dest.ModificationTime, opt => opt.Ignore()) + // .ForMember(dest => dest.InformatieObject, opt => opt.Ignore()) + // .ForMember(dest => dest.InformatieObjectId, opt => opt.Ignore()) + // .ForMember(dest => dest.RowVersion, opt => opt.Ignore()); + } + + //private static DataModel.AardRelatie AardRelatieFromString(string aardRelatie) + //{ + // ArgumentNullException.ThrowIfNull(aardRelatie); + + // if (!Enum.TryParse(aardRelatie.Trim(), out var result)) + // throw new InvalidOperationException($"AardRelatie {aardRelatie} not implemented."); + + // return result; + //} + + private static VertrouwelijkheidAanduiding? VertrouwelijkheidAanduidingFromString(string vertrouwelijkheidaanduiding) + { + if (string.IsNullOrEmpty(vertrouwelijkheidaanduiding)) + return null; + + if (!Enum.TryParse(vertrouwelijkheidaanduiding.Trim(), out var result)) + throw new InvalidOperationException($"VertrouwelijkheidAanduiding {vertrouwelijkheidaanduiding} not implemented."); + + return result; + } + + private static Status? StatusFromString(string status) + { + if (string.IsNullOrEmpty(status)) + return null; + + if (!Enum.TryParse(status.Trim(), out var result)) + throw new InvalidOperationException($"Status {status} not implemented."); + + return result; + } + + private static Algoritme AlgoritmeFromString(string algoritme) + { + ArgumentNullException.ThrowIfNull(algoritme); + + if (!Enum.TryParse(algoritme.Trim(), out var result)) + throw new InvalidOperationException($"Algoritme {algoritme} not implemented."); + + return result; + } + + private static Soort? SoortFromString(string soort) + { + if (string.IsNullOrEmpty(soort)) + return null; + + if (!Enum.TryParse(soort.Trim(), out var result)) + throw new InvalidOperationException($"Soort {soort} not implemented."); + + return result; + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/RequestToDomainProfile.cs b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/RequestToDomainProfile.cs index eee90628..d8b7082a 100644 --- a/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/RequestToDomainProfile.cs +++ b/src/OneGround.ZGW.Documenten.Web/MappingProfiles/v1/RequestToDomainProfile.cs @@ -72,6 +72,8 @@ public RequestToDomainProfile() .ForMember(dest => dest.Verschijningsvorm, opt => opt.Ignore()) .ForMember(dest => dest.Trefwoorden, opt => opt.Ignore()) .ForMember(dest => dest.InhoudIsVervallen, opt => opt.Ignore()) + .ForMember(dest => dest.IsGereedVoorPublicatie, opt => opt.Ignore()) + .ForMember(dest => dest.TonenAanInitiator, opt => opt.Ignore()) .ForMember(dest => dest.LatestInformatieObject, opt => opt.Ignore()) .ForMember(dest => dest.RowVersion, opt => opt.Ignore()); @@ -135,6 +137,8 @@ public RequestToDomainProfile() .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()) .ForMember(dest => dest.Owner, opt => opt.Ignore()) .ForMember(dest => dest.Verschijningsvorm, opt => opt.Ignore()) + .ForMember(dest => dest.IsGereedVoorPublicatie, opt => opt.Ignore()) + .ForMember(dest => dest.TonenAanInitiator, opt => opt.Ignore()) .ForMember(dest => dest.Trefwoorden, opt => opt.Ignore()) .ForMember(dest => dest.InhoudIsVervallen, opt => opt.Ignore()) .ForMember(dest => dest.LatestInformatieObject, opt => opt.Ignore()) diff --git a/src/OneGround.ZGW.Documenten.Web/Models/v1/7/GetAllEnkelvoudigInformatieObjectenFilter.cs b/src/OneGround.ZGW.Documenten.Web/Models/v1/7/GetAllEnkelvoudigInformatieObjectenFilter.cs new file mode 100644 index 00000000..c0d2b873 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Models/v1/7/GetAllEnkelvoudigInformatieObjectenFilter.cs @@ -0,0 +1,11 @@ +namespace OneGround.ZGW.Documenten.Web.Models.v1._7; + +public class GetAllEnkelvoudigInformatieObjectenFilter +{ + public string Bronorganisatie { get; set; } + public string Identificatie { get; set; } + public string[] Uuid_In { get; set; } = null; + public string[] Trefwoorden_In { get; set; } = null; + public string ObjectInformatieObjecten_Object { get; set; } + public string ObjectInformatieObjecten_ObjectType { get; set; } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Startup.cs b/src/OneGround.ZGW.Documenten.Web/Startup.cs index 1e831880..2b13d606 100644 --- a/src/OneGround.ZGW.Documenten.Web/Startup.cs +++ b/src/OneGround.ZGW.Documenten.Web/Startup.cs @@ -70,7 +70,7 @@ public void ConfigureServices(IServiceCollection services) services.AddZGWApi( "Documenten", Configuration, - Api.LatestVersion_1_5, + Api.LatestVersion_1_5, // TODO: If implemented all then we should set to 1.7 c => { c.MvcOptions = (o) => @@ -180,7 +180,7 @@ public void ConfigureServices(IServiceCollection services) services.Replace(ServiceDescriptor.Singleton()); // Register the query parameter validation filters globally - services.AddQueryParameterValidations(); + services.AddQueryAndSearchParameterValidations(); } public static void Configure(WebApplication app, IWebHostEnvironment env) diff --git a/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectCreateRequestValidator.cs b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectCreateRequestValidator.cs new file mode 100644 index 00000000..e216310c --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectCreateRequestValidator.cs @@ -0,0 +1,12 @@ +using OneGround.ZGW.Common.Web.Validations; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; + +namespace OneGround.ZGW.Documenten.Web.Validators.v1._7; + +public class EnkelvoudigInformatieObjectCreateRequestValidator : ZGWValidator +{ + public EnkelvoudigInformatieObjectCreateRequestValidator() + { + Include(new EnkelvoudigInformatieObjectRequestValidator()); + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectRequestValidator.cs b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectRequestValidator.cs new file mode 100644 index 00000000..21b4bca7 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectRequestValidator.cs @@ -0,0 +1,64 @@ +using System.IO; +using FluentValidation; +using OneGround.ZGW.Common.Contracts.v1; +using OneGround.ZGW.Common.DataModel; +using OneGround.ZGW.Common.Helpers; +using OneGround.ZGW.Common.MimeTypes; +using OneGround.ZGW.Common.Web.Validations; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; +using OneGround.ZGW.Documenten.DataModel; +using OneGround.ZGW.Documenten.Web.Extensions; + +namespace OneGround.ZGW.Documenten.Web.Validators.v1._7; + +public class EnkelvoudigInformatieObjectRequestValidator : ZGWValidator +{ + public EnkelvoudigInformatieObjectRequestValidator() + { + CascadeRuleFor(r => r.Identificatie).MaximumLength(40); + CascadeRuleFor(r => r.Bronorganisatie).IsRsin(required: true); + CascadeRuleFor(r => r.CreatieDatum).IsDate(true); + CascadeRuleFor(r => r.Titel).NotNull().NotEmpty().MaximumLength(200); + CascadeRuleFor(r => r.Vertrouwelijkheidaanduiding).IsEnumName(typeof(VertrouwelijkheidAanduiding)); + CascadeRuleFor(r => r.Auteur).NotNull().NotEmpty().MaximumLength(200); + CascadeRuleFor(r => r.Status).IsEnumName(typeof(Status)).When(r => !string.IsNullOrEmpty(r.Status)); + CascadeRuleFor(r => r.Formaat).IsValidMimeType(v => MimeTypeHelper.IsValidMimeType(v), maxLength: 255, allowEmpty: true); + CascadeRuleFor(r => r.Taal).IsIso639LanguageCode(required: true, ProfileHelper.Taal2letterTo3LetterMap); + CascadeRuleFor(r => r.Bestandsnaam).MaximumLength(255); + + CascadeRuleFor(r => r.Inhoud) + .NotNull() + .NotEmpty() + .Must(_ => false) // Fails always when there is an error file! + .WithMessage("Incorrect base64 data is specified.") + .WithErrorCode(ErrorCode.IncorrectBase64Padding) + .When(r => r.Inhoud != null && File.Exists(r.Inhoud + ".error")); // Note: this dummy error-file is created by Middleware when base64 decoding has failed! + + CascadeRuleFor(r => r.Inhoud) + .NotNull() + .NotEmpty() + .WithMessage("No base64 data is specified.") + .WithErrorCode(ErrorCode.Required) + .Must(inhoud => inhoud.IsAnyDocumentUrn() || File.Exists(inhoud)) // Note: inhoud is intercepted by Middleware which writes a file with base64 encoded data + .When(r => !string.IsNullOrEmpty(r.Inhoud)); // For base64-document (so not for multi-part or document-meta-only documents) + + CascadeRuleFor(r => r.Link).MaximumLength(200); + CascadeRuleFor(r => r.Beschrijving).MaximumLength(1000); + CascadeRuleFor(r => r.OntvangstDatum).IsDate(false); + CascadeRuleFor(r => r.VerzendDatum).IsDate(false); + CascadeRuleFor(r => r.Ondertekening) + .ChildRules(v => + { + v.CascadeRuleFor(r => r.Soort).NotNull().NotEmpty().IsEnumName(typeof(Soort)); + v.CascadeRuleFor(r => r.Datum).IsDate(true); + }); + CascadeRuleFor(r => r.Integriteit) + .ChildRules(v => + { + v.CascadeRuleFor(r => r.Algoritme).NotNull().NotEmpty().IsEnumName(typeof(Algoritme)); + v.CascadeRuleFor(r => r.Waarde).NotNull().NotEmpty().MaximumLength(128); + v.CascadeRuleFor(r => r.Datum).IsDate(true); + }); + CascadeRuleFor(r => r.InformatieObjectType).NotNull().NotEmpty().IsUri().MaximumLength(200); + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectSearchRequestValidator.cs b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectSearchRequestValidator.cs new file mode 100644 index 00000000..6f27c3c4 --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectSearchRequestValidator.cs @@ -0,0 +1,25 @@ +using FluentValidation; +using Microsoft.Extensions.Configuration; +using OneGround.ZGW.Common.Web.Validations; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; +using OneGround.ZGW.Documenten.Web.Configuration; + +namespace OneGround.ZGW.Documenten.Web.Validators.v1._7; + +public class EnkelvoudigInformatieObjectSearchRequestValidator : ZGWValidator +{ + public EnkelvoudigInformatieObjectSearchRequestValidator(IConfiguration configuration) + { + var applicationConfiguration = configuration.GetSection("Application").Get(); + + CascadeRuleFor(p => p.Expand) + .ExpandsValid(_5.Queries.SupportedExpands.GetAll("enkelvoudiginformatieobject")) + .IsExpandEnabled(applicationConfiguration.ExpandSettings.Search); + + CascadeRuleForEach(z => z.Uuid_In).IsGuid().WithName("uuid__in"); + CascadeRuleFor(p => p.ObjectInformatieObjecten_Object).IsUri(); + CascadeRuleFor(p => p.ObjectInformatieObjecten_ObjectType) + .IsEnumName(typeof(DataModel.ObjectType), caseSensitive: false) + .When(p => !string.IsNullOrEmpty(p.ObjectInformatieObjecten_ObjectType)); + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectUpdateRequestValidator.cs b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectUpdateRequestValidator.cs new file mode 100644 index 00000000..daed6a8f --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/EnkelvoudigInformatieObjectUpdateRequestValidator.cs @@ -0,0 +1,12 @@ +using OneGround.ZGW.Common.Web.Validations; +using OneGround.ZGW.Documenten.Contracts.v1._7.Requests; + +namespace OneGround.ZGW.Documenten.Web.Validators.v1._7; + +public class EnkelvoudigInformatieObjectUpdateRequestValidator : ZGWValidator +{ + public EnkelvoudigInformatieObjectUpdateRequestValidator() + { + Include(new EnkelvoudigInformatieObjectRequestValidator()); + } +} diff --git a/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/Queries/EnkelvoudigInformatieObjectenQueryParametersValidator.cs b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/Queries/EnkelvoudigInformatieObjectenQueryParametersValidator.cs new file mode 100644 index 00000000..6fabe03f --- /dev/null +++ b/src/OneGround.ZGW.Documenten.Web/Validators/v1/7/Queries/EnkelvoudigInformatieObjectenQueryParametersValidator.cs @@ -0,0 +1,25 @@ +using FluentValidation; +using Microsoft.Extensions.Configuration; +using OneGround.ZGW.Common.Web.Validations; +using OneGround.ZGW.Documenten.Contracts.v1._7.Queries; +using OneGround.ZGW.Documenten.Web.Configuration; + +namespace OneGround.ZGW.Documenten.Web.Validators.v1._7.Queries; + +public class EnkelvoudigInformatieObjectenQueryParametersValidator : ZGWValidator +{ + public EnkelvoudigInformatieObjectenQueryParametersValidator(IConfiguration configuration) + { + var applicationConfiguration = configuration.GetSection("Application").Get(); + + CascadeRuleFor(p => p.Expand) + .ExpandsValid(_5.Queries.SupportedExpands.GetAll("enkelvoudiginformatieobject")) + .IsExpandEnabled(applicationConfiguration.ExpandSettings.List); + + CascadeRuleFor(p => p.Bronorganisatie).IsRsin(required: false); + CascadeRuleFor(p => p.ObjectInformatieObjecten_Object).IsUri(); + CascadeRuleFor(p => p.ObjectInformatieObjecten_ObjectType) + .IsEnumName(typeof(DataModel.ObjectType), caseSensitive: false) + .When(p => !string.IsNullOrEmpty(p.ObjectInformatieObjecten_ObjectType)); + } +} diff --git a/src/OneGround.ZGW.Notificaties.Web/Startup.cs b/src/OneGround.ZGW.Notificaties.Web/Startup.cs index d4b35341..f6102310 100644 --- a/src/OneGround.ZGW.Notificaties.Web/Startup.cs +++ b/src/OneGround.ZGW.Notificaties.Web/Startup.cs @@ -26,7 +26,6 @@ using OneGround.ZGW.Notificaties.Messaging.Jobs.Extensions; using OneGround.ZGW.Notificaties.ServiceAgent.Extensions; using OneGround.ZGW.Notificaties.Web.Controllers; -using OneGround.ZGW.Notificaties.Web.Extensions; using OneGround.ZGW.Notificaties.Web.Services; using OneGround.ZGW.Notificaties.Web.Validators; diff --git a/src/Tests/OneGround.ZGW.Documenten.WebApi.UnitTests/BusinessRulesTests/v1/4/VerzendingBusinessRuleTests.cs b/src/Tests/OneGround.ZGW.Documenten.WebApi.UnitTests/BusinessRulesTests/v1/5/VerzendingBusinessRuleTests.cs similarity index 93% rename from src/Tests/OneGround.ZGW.Documenten.WebApi.UnitTests/BusinessRulesTests/v1/4/VerzendingBusinessRuleTests.cs rename to src/Tests/OneGround.ZGW.Documenten.WebApi.UnitTests/BusinessRulesTests/v1/5/VerzendingBusinessRuleTests.cs index 2c4c8e97..bda6d9d2 100644 --- a/src/Tests/OneGround.ZGW.Documenten.WebApi.UnitTests/BusinessRulesTests/v1/4/VerzendingBusinessRuleTests.cs +++ b/src/Tests/OneGround.ZGW.Documenten.WebApi.UnitTests/BusinessRulesTests/v1/5/VerzendingBusinessRuleTests.cs @@ -6,7 +6,7 @@ using OneGround.ZGW.Documenten.Web.BusinessRules.v1._5; using Xunit; -namespace OneGround.ZGW.Documenten.WebApi.UnitTests.BusinessRulesTests.v1._4; +namespace OneGround.ZGW.Documenten.WebApi.UnitTests.BusinessRulesTests.v1._5; public class VerzendingBusinessRuleTests { @@ -26,7 +26,7 @@ public void Add_With_Exactly_One_CorrespondentieAddress_Should_Be_Valid() var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.True(actual); Assert.Empty(errors); @@ -42,7 +42,7 @@ public void Add_With_Not_Any_CorrespondentieAddress_Should_Be_Invalid() var svc = new VerzendingBusinessRuleService(); var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.False(actual); Assert.Contains(errors, e => e.Name == "correspondentieAdres"); @@ -66,7 +66,7 @@ public void Add_With_More_Than_One_CorrespondentieAddress_Should_Be_Invalid() var svc = new VerzendingBusinessRuleService(); var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.False(actual); Assert.Contains(errors, e => e.Name == "correspondentieAdres"); @@ -88,7 +88,7 @@ public void Add_With_Afzender_Should_Contain_Ontvangstdatum_And_Empty_Verzenddat var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.True(actual); Assert.Empty(errors); @@ -110,7 +110,7 @@ public void Add_With_Geadresseerde_Should_Contain_Verzenddatum_And_Empty_Ontvang var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.True(actual); Assert.Empty(errors); @@ -133,7 +133,7 @@ public void Add_With_Geadresseerde_With_Ontvangstdatum_And_Verzenddatum_Should_F var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.False(actual); Assert.Contains(errors, e => e.Name == "ontvangstdatum" && e.Code == ErrorCode.MustBeEmpty); @@ -156,7 +156,7 @@ public void Add_With_Afzender_With_Ontvangstdatum_And_Verzenddatum_Should_Fail() var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.False(actual); Assert.Contains(errors, e => e.Name == "verzenddatum" && e.Code == ErrorCode.MustBeEmpty); @@ -173,12 +173,29 @@ public void Add_With_Geadresseerde_Without_Ontvangstdatum_And_Without_Verzenddat var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.False(actual); Assert.Contains(errors, e => e.Name == "verzenddatum" && e.Code == ErrorCode.Required); } + [Fact] + public void Add_With_Geadresseerde_Without_Verzenddatum_Should_Succeed_In_Versie_1_7() + { + var informatieobject = new EnkelvoudigInformatieObject(); + + var verzending = new Verzending { AardRelatie = AardRelatie.geadresseerde, MijnOverheid = true }; + + var svc = new VerzendingBusinessRuleService(); + + var errors = new List(); + + var actual = svc.Validate(informatieobject, verzending, 1.7M, errors); + + Assert.True(actual); + Assert.Empty(errors); + } + [Fact] public void Add_With_Afzender_Without_Ontvangstdatum_And_Without_Verzenddatum_Should_Fail() { @@ -190,7 +207,7 @@ public void Add_With_Afzender_Without_Ontvangstdatum_And_Without_Verzenddatum_Sh var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.False(actual); Assert.Contains(errors, e => e.Name == "ontvangstdatum" && e.Code == ErrorCode.Required); @@ -228,7 +245,7 @@ public void Add_With_An_Already_Existing_Afzender_Should_Fail() var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.False(actual); Assert.Contains(errors, e => e.Name == "nonFieldErrors" && e.Code == ErrorCode.Invalid); @@ -266,7 +283,7 @@ public void Add_With_An_Already_Existing_Geadresseerde_Should_Be_valid() var errors = new List(); - var actual = svc.Validate(informatieobject, verzending, errors); + var actual = svc.Validate(informatieobject, verzending, 1.5M, errors); Assert.True(actual); Assert.Empty(errors); @@ -312,7 +329,7 @@ public void Update_With_An_Already_Existing_Afzender_With_The_Same_InformatieObj var errors = new List(); - var actual = svc.Validate(informatieobject, existingVerzending: verzending, existingVerzendingId, errors); + var actual = svc.Validate(informatieobject, existingVerzending: verzending, existingVerzendingId, 1.5M, errors); Assert.False(actual); Assert.Contains(errors, e => e.Name == "nonFieldErrors" && e.Code == ErrorCode.Invalid); @@ -376,7 +393,7 @@ public void Update_With_An_Already_Existing_Afzender_With_Another_InformatieObje var errors = new List(); - var actual = svc.Validate(anotherInformatieobject, existingVerzending: verzending, existingVerzendingId, errors); + var actual = svc.Validate(anotherInformatieobject, existingVerzending: verzending, existingVerzendingId, 1.5M, errors); Assert.True(actual); Assert.Empty(errors);