diff --git a/Directory.Build.props b/Directory.Build.props index 67b262d934..47c0576132 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 9.11.0 + 9.12.0-alpha.1 13.0 Jeremy D. Miller;Babu Annamalai;Jaedyn Tonee https://martendb.io/logo.png diff --git a/Directory.Packages.props b/Directory.Packages.props index fa91385ada..26a238be44 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -73,13 +73,13 @@ AggregateType (TypeDescriptor), populated in the shared SubscriptionDescriptor(ISubscriptionSource, IEventStore) ctor that Marten's projections funnel through via ProjectionGraph.Describe. See marten#4772. --> - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Marten/DocumentStore.DocumentDiagnostics.cs b/src/Marten/DocumentStore.DocumentDiagnostics.cs index ea52340906..af1e9e8bb7 100644 --- a/src/Marten/DocumentStore.DocumentDiagnostics.cs +++ b/src/Marten/DocumentStore.DocumentDiagnostics.cs @@ -5,7 +5,9 @@ using System.Threading; using System.Threading.Tasks; using JasperFx.Core.Reflection; +using JasperFx.Descriptors; using JasperFx.Documents; +using JasperFx.MultiTenancy; using Marten.Schema; namespace Marten; @@ -44,13 +46,27 @@ async Task IDocumentStoreDiagnostics.QueryDocumentsAsync( } var table = mapping.TableName.QualifiedName; - var where = options.IdEquals != null ? " where id::text = @id" : ""; var pageNumber = Math.Max(1, options.PageNumber); var pageSize = Math.Max(1, options.PageSize); var offset = (pageNumber - 1) * pageSize; - await using var conn = Tenancy.Default.Database.CreateConnection(); + // Tenant scoping (#475 / EVENT_STORE_EXPLORER_PLAN §3.1): a database-per-tenant + // store selects the tenant's physical database; conjoined tenancy keeps every + // tenant in one database with a tenant_id column we filter on. + var database = options.TenantId != null && Tenancy.Cardinality != DatabaseCardinality.Single + ? await Tenancy.FindOrCreateDatabase(options.TenantId).ConfigureAwait(false) + : Tenancy.Default.Database; + var filterByTenant = options.TenantId != null + && Tenancy.Cardinality == DatabaseCardinality.Single + && mapping.TenancyStyle == TenancyStyle.Conjoined; + + var conditions = new List(); + if (options.IdEquals != null) conditions.Add("id::text = @id"); + if (filterByTenant) conditions.Add("tenant_id = @tenant"); + var where = conditions.Count > 0 ? " where " + string.Join(" and ", conditions) : ""; + + await using var conn = database.CreateConnection(); await conn.OpenAsync(token).ConfigureAwait(false); long total; @@ -61,6 +77,10 @@ async Task IDocumentStoreDiagnostics.QueryDocumentsAsync( { countCmd.Parameters.AddWithValue("id", options.IdEquals); } + if (filterByTenant) + { + countCmd.Parameters.AddWithValue("tenant", options.TenantId!); + } total = Convert.ToInt64(await countCmd.ExecuteScalarAsync(token).ConfigureAwait(false)); } @@ -74,6 +94,10 @@ async Task IDocumentStoreDiagnostics.QueryDocumentsAsync( { cmd.Parameters.AddWithValue("id", options.IdEquals); } + if (filterByTenant) + { + cmd.Parameters.AddWithValue("tenant", options.TenantId!); + } await using var reader = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false); while (await reader.ReadAsync(token).ConfigureAwait(false)) diff --git a/src/Marten/DocumentStore.DocumentStoreUsage.cs b/src/Marten/DocumentStore.DocumentStoreUsage.cs index dd54c080f1..ce5d7f8768 100644 --- a/src/Marten/DocumentStore.DocumentStoreUsage.cs +++ b/src/Marten/DocumentStore.DocumentStoreUsage.cs @@ -57,6 +57,21 @@ public partial class DocumentStore : IDocumentStoreUsageSource // Configured/Default masking, etc.). ApplyFlatOptionValues(usage); + // jasperfx#475 — advertise which document metadata Marten captures so + // store-aware consumers (CritterWatch) gate document-query facets by what is + // actually persisted. Version / last-modified / tenant / soft-delete are + // universal facets in Marten and keep the descriptor's default of true; the + // opt-in columns (correlation/causation/last-modified-by) are only queryable + // where some document mapping has enabled them. + var mappings = Options.Storage.DocumentMappingsWithSchema.ToList(); + usage.DocumentMetadata = new DocumentMetadataCapabilities + { + StoreType = "Marten", + CorrelationId = mappings.Any(m => m.Metadata.CorrelationId.Enabled), + CausationId = mappings.Any(m => m.Metadata.CausationId.Enabled), + LastModifiedBy = mappings.Any(m => m.Metadata.LastModifiedBy.Enabled) + }; + return usage; } diff --git a/src/Marten/DocumentStore.EventStore.cs b/src/Marten/DocumentStore.EventStore.cs index 735b7c0f9e..691ba7cce1 100644 --- a/src/Marten/DocumentStore.EventStore.cs +++ b/src/Marten/DocumentStore.EventStore.cs @@ -565,6 +565,23 @@ async Task ISubscriptionRunner.ExecuteAsync(ISubscription subscri usage.GlobalAggregates.Add(TypeDescriptor.For(aggregateType)); } + // jasperfx#475 — advertise WHICH event/stream metadata this store captures + // so store-aware consumers (CritterWatch) can gate query facets by what is + // actually persisted, rather than sniffing Marten's MetadataConfig directly. + // The event flags are opt-in columns and map straight off MetadataConfig. + // The stream flags are universal in Marten (mt_streams always exposes the + // aggregate-type / version / created+updated / tenant / archived columns), + // so they keep the EventMetadataCapabilities defaults of true. + var metadata = Options.EventGraph.MetadataConfig; + usage.EventMetadata = new EventMetadataCapabilities + { + StoreType = "Marten", + CorrelationId = metadata.CorrelationIdEnabled, + CausationId = metadata.CausationIdEnabled, + Headers = metadata.HeadersEnabled, + UserName = metadata.UserNameEnabled + }; + return usage; }