From 3d9e54894891dfc3022f65aea699aa749e163ec6 Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Sun, 1 Nov 2026 16:23:30 +0300 Subject: [PATCH 1/2] fix: dashboard failures and retry summary timing out on big databases --- .../Resources/Dashboard/FailuresToActOn.cs | 47 + .../Resources/Dashboard/RetrySummary.cs | 36 +- .../Tests/RetryChainTests.cs | 67 + SW.Bitween.PgSql/BitweenDbContext.cs | 13 + ...0261101132211_DashboardIndexes.Designer.cs | 4209 +++++++++++++++++ .../20261101132211_DashboardIndexes.cs | 33 + .../BitweenDbContextModelSnapshot.cs | 12 + .../ClientApp/e2e/dashboard.spec.ts | 2 + SW.Bitween.Web/ClientApp/src/api/client.ts | 12 + .../ClientApp/src/api/http/dashboard.ts | 68 +- SW.Bitween.Web/ClientApp/src/api/queryKeys.ts | 4 + SW.Bitween.Web/ClientApp/src/api/types.ts | 19 +- .../src/pages/dashboard/DashboardPage.tsx | 61 +- .../__tests__/DashboardPage.test.tsx | 41 +- docs/api-reference.md | 3 +- docs/operations.md | 2 +- 16 files changed, 4568 insertions(+), 61 deletions(-) create mode 100644 SW.Bitween.Api/Resources/Dashboard/FailuresToActOn.cs create mode 100644 SW.Bitween.PgSql/Migrations/20261101132211_DashboardIndexes.Designer.cs create mode 100644 SW.Bitween.PgSql/Migrations/20261101132211_DashboardIndexes.cs diff --git a/SW.Bitween.Api/Resources/Dashboard/FailuresToActOn.cs b/SW.Bitween.Api/Resources/Dashboard/FailuresToActOn.cs new file mode 100644 index 00000000..0a63988a --- /dev/null +++ b/SW.Bitween.Api/Resources/Dashboard/FailuresToActOn.cs @@ -0,0 +1,47 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Resources.Xchanges; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Dashboard; + +/// +/// How many failures nothing has been retried from yet: the ends of chains, the problems still to +/// deal with. The number the exchange list's failed + newest-attempt view counts, counted here from +/// the failures rather than from the exchanges. +/// +/// +/// The dashboard used to ask the list for it, and the list fetches its rows newest first: on a +/// database where nearly every failure gets retried, that walked back through millions of exchanges +/// before reaching one, past the 30s limit - for a row the dashboard never showed. Here the failures +/// come from their own index (ix_xchange_result_failed on Postgres) and the retry check from +/// RetryFor's, so no exchange row is read. Capped like the list's count, so the client shows +/// "10,000+" for either. +/// +[HandlerName("failurestoacton")] +public class FailuresToActOn : IQueryHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public FailuresToActOn(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle() + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Dashboard.View); + + var total = await _dbContext.Set().AsNoTracking() + .Where(result => !result.Success + && !_dbContext.Set().Any(child => child.RetryFor == result.Id)) + .Take(Search.CountCap + 1) + .CountAsync(); + + return new { Total = total }; + } +} diff --git a/SW.Bitween.Api/Resources/Dashboard/RetrySummary.cs b/SW.Bitween.Api/Resources/Dashboard/RetrySummary.cs index d59ad8b9..3e9c56b8 100644 --- a/SW.Bitween.Api/Resources/Dashboard/RetrySummary.cs +++ b/SW.Bitween.Api/Resources/Dashboard/RetrySummary.cs @@ -67,32 +67,42 @@ group result by result.Success && !result.ResponseBad into worked // Chains that keep failing: a failure nothing has been retried from - so it is where that // chain has got to - which is itself a retry, so the chain is at least two attempts long. + // + // Over the same seven days as the number above, and asked so that indexes answer all of it: + // the retries from their own (ix_xchange_retries_started_on on Postgres), whether each + // failed from the failures' and whether it was retried from RetryFor's. With no time bound + // it walked back through every retry ever made until it found five hundred failures - on a + // database where every failure gets retried, millions of exchanges, past the 30s limit. var candidates = await ( from xchange in _dbContext.Set() - join result in _dbContext.Set() on xchange.Id equals result.Id where xchange.RetryFor != null - && !result.Success + && xchange.StartedOn >= since + && _dbContext.Set().Any(result => result.Id == xchange.Id && !result.Success) && !_dbContext.Set().Any(child => child.RetryFor == xchange.Id) orderby xchange.StartedOn descending - select new - { - xchange.Id, - xchange.RetryFor, - xchange.SubscriptionId, - xchange.DocumentId, - xchange.StartedOn, - result.Exception - }).AsNoTracking().Take(Candidates).ToListAsync(); + select new { xchange.Id, xchange.RetryFor, xchange.StartedOn }) + .AsNoTracking().Take(Candidates).ToListAsync(); var attempts = await CountAttempts(candidates.ToDictionary(c => c.Id, c => c.RetryFor)); - var worst = candidates + var worstIds = candidates .OrderByDescending(c => attempts[c.Id]) .ThenByDescending(c => c.StartedOn) .Take(Listed) + .Select(c => c.Id) + .ToList(); + + // What is shown of them, and the names below, for the few that are actually listed rather + // than for all five hundred. + var worst = (await ( + from xchange in _dbContext.Set() + join result in _dbContext.Set() on xchange.Id equals result.Id + where worstIds.Contains(xchange.Id) + select new { xchange.Id, xchange.SubscriptionId, xchange.DocumentId, xchange.StartedOn, result.Exception }) + .AsNoTracking().ToListAsync()) + .OrderBy(w => worstIds.IndexOf(w.Id)) .ToList(); - // Names for the few that are actually listed, rather than for all five hundred. var subscriptionIds = worst.Where(w => w.SubscriptionId != null).Select(w => w.SubscriptionId!.Value).ToList(); var subscriptionNames = await _dbContext.Set().AsNoTracking() .Where(s => subscriptionIds.Contains(s.Id)) diff --git a/SW.Bitween.IntegrationTests/Tests/RetryChainTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryChainTests.cs index ef64f8f0..d1b012db 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryChainTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryChainTests.cs @@ -530,6 +530,73 @@ public async Task RetrySummary_ranks_the_chains_that_keep_failing_and_says_wheth Assert.True(retries.GetProperty("Finished").GetInt32() >= retries.GetProperty("Succeeded").GetInt32()); } + /// + /// The panel looks at the same seven days as its retry numbers: a chain nothing has retried for + /// longer is still a failure to act on, but no longer one that keeps failing. + /// + [Fact] + public async Task RetrySummary_leaves_out_chains_nothing_has_retried_for_a_week() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var (_, first) = await FailedXchange(db, xs, "Summary Old Doc"); + var second = await RetryOnce(db, xs, ctx, first.Id); + var third = await RetryOnce(db, xs, ctx, second.Id); + var chain = new[] { first.Id, second.Id, third.Id }; + await db.Set().Where(x => chain.Contains(x.Id)) + .ExecuteUpdateAsync(s => s.SetProperty(x => x.StartedOn, x => x.StartedOn.AddDays(-8))); + + var summary = await new Resources.Dashboard.RetrySummary(db, ctx).Handle(); + var json = System.Text.Json.JsonSerializer.SerializeToElement(summary); + + Assert.DoesNotContain(json.GetProperty("FailingChains").EnumerateArray(), + c => c.GetProperty("Id").GetString() == third.Id); + } + + /// + /// The dashboard's failures-to-act-on tile counts from the failures rather than through the + /// exchange list, and has to come to the number the list shows for the view the tile opens. + /// + [Fact] + public async Task FailuresToActOn_counts_what_the_list_of_failures_to_act_on_counts() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var tile = new Resources.Dashboard.FailuresToActOn(db, ctx); + async Task Total() => + System.Text.Json.JsonSerializer.SerializeToElement(await tile.Handle()).GetProperty("Total").GetInt32(); + var before = await Total(); + + // A chain of three failed attempts is one failure to act on: the attempt at its end. + var (sub, first) = await FailedXchange(db, xs, "To Act On Doc"); + var second = await RetryOnce(db, xs, ctx, first.Id); + await RetryOnce(db, xs, ctx, second.Id); + + // A failure whose retry worked leaves nothing to act on. + var healed = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + db.Set().Add(new XchangeResult(healed.Id, null, null, exception: "boom")); + await db.SaveChangesAsync(); + await new Resources.Xchanges.Retry(db, ctx, xs).Handle(healed.Id, new XchangeRetry()); + await db.SaveChangesAsync(); + var healedRetry = await db.Set().FirstAsync(x => x.RetryFor == healed.Id); + db.Set().Add(new XchangeResult(healedRetry.Id, null, null)); + await db.SaveChangesAsync(); + + var after = await Total(); + Assert.Equal(before + 1, after); + + var listed = (SearchyResponse)await new Resources.Xchanges.Search(db, xs, ctx) + .Handle(new SearchyRequest("filter=StatusFilter:1:3&filter=LatestOnly:1:true") { PageSize = 1 }); + Assert.Equal(listed.TotalCount, after); + } + // ─── Reading the chain back ─────────────────────────────────────────────── [Fact] diff --git a/SW.Bitween.PgSql/BitweenDbContext.cs b/SW.Bitween.PgSql/BitweenDbContext.cs index 77fdc107..eba1655e 100644 --- a/SW.Bitween.PgSql/BitweenDbContext.cs +++ b/SW.Bitween.PgSql/BitweenDbContext.cs @@ -407,6 +407,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasIndex(i => i.SubscriptionId); b.HasIndex(i => i.StartedOn); b.HasIndex(i => i.RetryFor); + // Retries newest first, for the dashboard's retry panel: a few percent of the + // exchanges, read without touching the rows. + b.HasIndex(i => i.StartedOn, "ix_xchange_retries_started_on") + .HasDatabaseName("ix_xchange_retries_started_on") + .HasFilter("retry_for IS NOT NULL") + .IncludeProperties(i => new { i.Id, i.RetryFor }) + .IsCreatedConcurrently(); }); modelBuilder.Entity(b => @@ -438,6 +445,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.HandlerVersion).HasMaxLength(50); // A subscription's latest result: what auto-pause counts from. b.HasIndex(p => new { p.SubscriptionId, p.FinishedOn }).IsCreatedConcurrently(); + // The failures alone, a few percent of the results: what the dashboard counts the + // failures still to act on from, and how it asks whether a retry failed. + b.HasIndex(p => p.Id, "ix_xchange_result_failed") + .HasDatabaseName("ix_xchange_result_failed") + .HasFilter("NOT success") + .IsCreatedConcurrently(); b.HasOne().WithOne().HasForeignKey(p => p.Id).OnDelete(DeleteBehavior.Cascade); }); diff --git a/SW.Bitween.PgSql/Migrations/20261101132211_DashboardIndexes.Designer.cs b/SW.Bitween.PgSql/Migrations/20261101132211_DashboardIndexes.Designer.cs new file mode 100644 index 00000000..fccc0ee9 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261101132211_DashboardIndexes.Designer.cs @@ -0,0 +1,4209 @@ +// +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 SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261101132211_DashboardIndexes")] + partial class DashboardIndexes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("Kind") + .HasColumnType("smallint") + .HasColumnName("kind"); + + b.Property("LastSignInOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in_on"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("OidcIdentity") + .HasColumnType("text") + .HasColumnName("oidc_identity"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("email_error"); + + b.Property("EmailedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("emailed_on"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("hash"); + + b.Property("Purpose") + .HasColumnType("smallint") + .HasColumnName("purpose"); + + b.HasKey("Id") + .HasName("pk_account_links"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_links_account_id"); + + b.HasIndex("Hash") + .IsUnique() + .HasDatabaseName("ix_account_links_hash"); + + b.ToTable("AccountLinks", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("hash"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("last_used_from"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("revoked_by"); + + b.Property("RevokedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_on"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("shown"); + + b.HasKey("Id") + .HasName("pk_service_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_service_tokens_account_id"); + + b.HasIndex("Hash") + .IsUnique() + .HasDatabaseName("ix_service_tokens_hash"); + + b.ToTable("ServiceTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key_name"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.HasKey("PartnerId", "KeyName") + .HasName("pk_api_key_use"); + + b.ToTable("api_key_use", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.BackgroundRequest", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Done") + .HasColumnType("integer") + .HasColumnName("done"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Label") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("label"); + + b.Property("Node") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("node"); + + b.Property("Owner") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("owner"); + + b.Property("Page") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("page"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("path"); + + b.Property("Result") + .HasColumnType("bytea") + .HasColumnName("result"); + + b.Property("ResultContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("result_content_type"); + + b.Property("ResultDisposition") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("result_disposition"); + + b.Property("ResultKey") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("result_key"); + + b.Property("ResultStatus") + .HasColumnType("integer") + .HasColumnName("result_status"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("summary"); + + b.Property("Total") + .HasColumnType("integer") + .HasColumnName("total"); + + b.HasKey("Id") + .HasName("pk_background_request"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_background_request_started_on"); + + b.HasIndex("Owner", "StartedOn") + .HasDatabaseName("ix_background_request_owner_started_on"); + + b.ToTable("background_request", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("OutboxPublishedLastHour") + .HasColumnType("integer") + .HasColumnName("outbox_published_last_hour"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("resident_health"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Connections.Connection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .HasColumnType("text") + .HasColumnName("access_token"); + + b.Property("AccessTokenExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("access_token_expires_on"); + + b.Property("AuthorizeParameters") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("authorize_parameters"); + + b.Property("AuthorizeUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("authorize_url"); + + b.Property("AuthorizedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("authorized_by"); + + b.Property("AuthorizedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("authorized_on"); + + b.Property("ClientId") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Grant") + .HasColumnType("integer") + .HasColumnName("grant"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("RenewingUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("renewing_until"); + + b.Property("Scopes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("scopes"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TokenAuth") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("token_auth"); + + b.Property("TokenUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("token_url"); + + b.Property("Username") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_connections"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_connections_name"); + + b.ToTable("Connections", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DropDuplicates") + .HasColumnType("boolean") + .HasColumnName("drop_duplicates"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DropDuplicates = false, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DuplicateArrival", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Dropped") + .HasColumnType("boolean") + .HasColumnName("dropped"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("OriginalXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("original_xchange_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.HasKey("Id") + .HasName("pk_duplicate_arrival"); + + b.HasIndex("DocumentId", "OccurredOn") + .HasDatabaseName("ix_duplicate_arrival_document_id_occurred_on"); + + b.ToTable("duplicate_arrival", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FailureAnswer") + .HasColumnType("integer") + .HasColumnName("failure_answer"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.Property("Role") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("role"); + + b.Property("Runtime") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("runtime"); + + b.Property("Calls") + .HasColumnType("integer") + .HasColumnName("calls"); + + b.Property("Crashes") + .HasColumnType("integer") + .HasColumnName("crashes"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failures") + .HasColumnType("integer") + .HasColumnName("failures"); + + b.Property("LimitStops") + .HasColumnType("integer") + .HasColumnName("limit_stops"); + + b.Property("Timeouts") + .HasColumnType("integer") + .HasColumnName("timeouts"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime") + .HasName("pk_adapter_stats"); + + b.ToTable("adapter_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("Scope") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("scope"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope_key"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("fingerprint"); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("sample"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint") + .HasName("pk_error_stats"); + + b.ToTable("error_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("BadResponse") + .HasColumnType("integer") + .HasColumnName("bad_response"); + + b.Property("BytesIn") + .HasColumnType("bigint") + .HasColumnName("bytes_in"); + + b.Property("BytesOut") + .HasColumnType("bigint") + .HasColumnName("bytes_out"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DbCommands") + .HasColumnType("bigint") + .HasColumnName("db_commands"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failed") + .HasColumnType("integer") + .HasColumnName("failed"); + + b.Property("FailedAtHandler") + .HasColumnType("integer") + .HasColumnName("failed_at_handler"); + + b.Property("FailedAtMapper") + .HasColumnType("integer") + .HasColumnName("failed_at_mapper"); + + b.Property("ManualRetries") + .HasColumnType("integer") + .HasColumnName("manual_retries"); + + b.Property("PayloadParses") + .HasColumnType("bigint") + .HasColumnName("payload_parses"); + + b.Property("Retries") + .HasColumnType("integer") + .HasColumnName("retries"); + + b.Property("Succeeded") + .HasColumnType("integer") + .HasColumnName("succeeded"); + + b.Property("Total") + .HasColumnType("integer") + .HasColumnName("total"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId") + .HasName("pk_exchange_stats"); + + b.ToTable("exchange_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property("AckRate") + .HasColumnType("double precision") + .HasColumnName("ack_rate"); + + b.Property("Consumers") + .HasColumnType("integer") + .HasColumnName("consumers"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint") + .HasColumnName("dead_letter_backlog"); + + b.Property("IncomingRate") + .HasColumnType("double precision") + .HasColumnName("incoming_rate"); + + b.Property("QueueDepth") + .HasColumnType("bigint") + .HasColumnName("queue_depth"); + + b.Property("RetryBacklog") + .HasColumnType("bigint") + .HasColumnName("retry_backlog"); + + b.Property("UnhealthyConsumers") + .HasColumnType("integer") + .HasColumnName("unhealthy_consumers"); + + b.HasKey("At") + .HasName("pk_queue_snapshot"); + + b.ToTable("queue_snapshot", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.NotificationChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("HandlerId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_notification_channel"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_notification_channel_name"); + + b.ToTable("notification_channel", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("detail"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("target"); + + b.HasKey("Id") + .HasName("pk_operator_action"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_operator_action_occurred_on"); + + b.ToTable("operator_action", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_outbox_message_created_on") + .HasFilter("published_on IS NULL") + .HasAnnotation("Npgsql:CreatedConcurrently", true); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertChannelId") + .HasColumnType("integer") + .HasColumnName("alert_channel_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertChannelId") + .HasColumnType("integer") + .HasColumnName("alert_channel_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("ExpectTraffic") + .HasColumnType("text") + .HasColumnName("expect_traffic"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Notifications") + .HasColumnType("text") + .HasColumnName("notifications"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("QuietSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("quiet_since"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Number") + .HasColumnType("integer") + .HasColumnName("number"); + + b.Property("SavedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("saved_by"); + + b.Property("SavedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("saved_on"); + + b.Property("Snapshot") + .IsRequired() + .HasColumnType("text") + .HasColumnName("snapshot"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_revision"); + + b.HasIndex("SubscriptionId", "Number") + .IsUnique() + .HasDatabaseName("ix_subscription_revision_subscription_id_number"); + + b.ToTable("subscription_revision", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.HasIndex(new[] { "StartedOn" }, "ix_xchange_retries_started_on") + .HasDatabaseName("ix_xchange_retries_started_on") + .HasFilter("retry_for IS NOT NULL") + .HasAnnotation("Npgsql:CreatedConcurrently", true); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex(new[] { "StartedOn" }, "ix_xchange_retries_started_on"), new[] { "Id", "RetryFor" }); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("integer") + .HasColumnName("channel_id"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("ChannelId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_channel_id_finished_on"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("DurationMs") + .HasColumnType("integer") + .HasColumnName("duration_ms"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("NodeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node_name"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Warning") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("warning"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.HasIndex("SubscriptionId", "FinishedOn") + .HasDatabaseName("ix_xchange_result_subscription_id_finished_on") + .HasAnnotation("Npgsql:CreatedConcurrently", true); + + b.HasIndex(new[] { "Id" }, "ix_xchange_result_failed") + .HasDatabaseName("ix_xchange_result_failed") + .HasFilter("NOT success") + .HasAnnotation("Npgsql:CreatedConcurrently", true); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeRetryClaim", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("ClaimedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_on"); + + b.Property("RetryXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_retry_claim"); + + b.ToTable("xchange_retry_claim", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_links_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_service_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionRevision", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_revision_subscription_subscription_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeRetryClaim", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeRetryClaim", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_retry_claim_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261101132211_DashboardIndexes.cs b/SW.Bitween.PgSql/Migrations/20261101132211_DashboardIndexes.cs new file mode 100644 index 00000000..fe5b41ca --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261101132211_DashboardIndexes.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + /// The failed results and the retries, each in an index of its own, so the dashboard counts the + /// failures still to act on and finds the chains that keep failing without walking every + /// exchange. Both are a few percent of their table. Built without stopping writes to it. + /// + public partial class DashboardIndexes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Outside the transaction, on what can be the largest tables there are: see ConcurrentIndex. + ConcurrentIndex.Create(migrationBuilder, "xchange_result", "ix_xchange_result_failed", + "(id) WHERE NOT success"); + + ConcurrentIndex.Create(migrationBuilder, "xchange", "ix_xchange_retries_started_on", + "(started_on) INCLUDE (id, retry_for) WHERE retry_for IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ConcurrentIndex.Drop(migrationBuilder, "ix_xchange_result_failed"); + + ConcurrentIndex.Drop(migrationBuilder, "ix_xchange_retries_started_on"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs index f04b40d5..d074251a 100644 --- a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -2929,6 +2929,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SubscriptionId") .HasDatabaseName("ix_xchange_subscription_id"); + b.HasIndex(new[] { "StartedOn" }, "ix_xchange_retries_started_on") + .HasDatabaseName("ix_xchange_retries_started_on") + .HasFilter("retry_for IS NOT NULL") + .HasAnnotation("Npgsql:CreatedConcurrently", true); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex(new[] { "StartedOn" }, "ix_xchange_retries_started_on"), new[] { "Id", "RetryFor" }); + b.ToTable("xchange", "infolink"); }); @@ -3165,6 +3172,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDatabaseName("ix_xchange_result_subscription_id_finished_on") .HasAnnotation("Npgsql:CreatedConcurrently", true); + b.HasIndex(new[] { "Id" }, "ix_xchange_result_failed") + .HasDatabaseName("ix_xchange_result_failed") + .HasFilter("NOT success") + .HasAnnotation("Npgsql:CreatedConcurrently", true); + b.ToTable("xchange_result", "infolink"); }); diff --git a/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts b/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts index f192974e..823bf477 100644 --- a/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts @@ -19,6 +19,8 @@ test("dashboard loads with real aggregated data", async ({ page }) => { // which an unanchored match picks up as well. const tile = page.getByRole("link").filter({ hasText: /^Failures to act on/ }); await expect(tile).toBeVisible(); + // Counted apart from the rest of the page, so it can fill in after it. + await expect(tile).toContainText(/\d/, { timeout: 15000 }); const count = (await tile.innerText()).match(/([\d,]+)/)?.[1]; expect(count).toBeTruthy(); diff --git a/SW.Bitween.Web/ClientApp/src/api/client.ts b/SW.Bitween.Web/ClientApp/src/api/client.ts index c54c3605..e16c9404 100644 --- a/SW.Bitween.Web/ClientApp/src/api/client.ts +++ b/SW.Bitween.Web/ClientApp/src/api/client.ts @@ -39,6 +39,7 @@ import type { DataSourceTelemetry, DataSourceTestResult, DashboardData, + RetrySummary, ExchangeBreakdown, ExchangeQuery, ExchangeRow, @@ -825,6 +826,17 @@ export interface ApiClient { // - dashboard - getDashboard(): Promise; + // The counts over the exchange tables, which grow with traffic: asked for apart, so a slow one + // fills in on its own tile rather than holding up the page. + /** + * Failures that are the newest attempt of their chain. Counts problems rather than attempts: a + * chain retried nine times is one of these, not nine. Capped like the exchange search, so a value + * above 10,000 means "more than that". + */ + getFailuresToActOn(): Promise; + /** Exchanges started in the last 24 hours and not finished yet. */ + getInFlight(): Promise; + getRetrySummary(): Promise; // Metrics, read from the hourly rows the server keeps. getMetricsOverview(): Promise; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts index 9f7be4a0..11e9d835 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts @@ -1,5 +1,5 @@ import type { ApiClient } from "../client"; -import type { DashboardData, ExchangeStatus } from "../types"; +import type { DashboardData, ExchangeStatus, RetrySummary } from "../types"; import { subscriptionMethods } from "./subscriptions"; import { get } from "./request"; @@ -38,41 +38,35 @@ const toStatus = (raw: Pick): const LATEST = 6; +const startedSince = (ms: number) => encodeURIComponent(`StartedOn:6:${new Date(Date.now() - ms).toISOString()}`); + export const dashboardMethods = { /** * What the dashboard shows beside the metrics (which come from /metrics/overview): the latest - * failures, what is in flight, retries, queue alerts and subscriptions needing attention. + * failures, pending retries, queue alerts and subscriptions needing attention. * * Traffic, rates and the busiest used to be counted here, in the browser, from up to 1000 * exchanges fetched every minute: wrong at any volume above that, and a heavy query for every * open dashboard. They come from the server's hourly metrics now; what is left here is small, - * bounded queries over the last two weeks. + * bounded queries over the last two weeks. The counts over the exchange tables are asked for + * apart, below. */ async getDashboard(): Promise { - const now = Date.now(); - const since = (ms: number) => encodeURIComponent(`StartedOn:6:${new Date(now - ms).toISOString()}`); - const twoWeeks = since(14 * 86_400_000); + const twoWeeks = startedSince(14 * 86_400_000); const latest = (status: number) => get>( `/xchanges?filter=${twoWeeks}&filter=${encodeURIComponent(`StatusFilter:1:${status}`)}&sort=StartedOn:2&size=${LATEST}`, ); - const [failed, badResponses, inFlight, delayedRes, failuresRes, retrySummary, alertsRaw, subscriptionRows] = - await Promise.all([ - latest(3), - latest(2), - get>(`/xchanges?filter=${since(86_400_000)}&filter=${encodeURIComponent("StatusFilter:1:0")}&size=1`), - get>("/delayedretries?size=1"), - // Only the total is wanted: the problems still to deal with, which the tile's list opens. - get>( - `/xchanges?filter=${encodeURIComponent("StatusFilter:1:3")}&filter=${encodeURIComponent("LatestOnly:1:true")}&size=1`, - ), - get("/dashboard/retrysummary"), - // Depends on RabbitMQ management being configured on the backend - don't let it take the - // rest of the dashboard down when it isn't; the "Queue alerts" tile flags it instead. - get("/ops/alerts").catch(() => null), - subscriptionMethods.listSubscriptionRows(), - ]); + const [failed, badResponses, delayedRes, alertsRaw, subscriptionRows] = await Promise.all([ + latest(3), + latest(2), + get>("/delayedretries?size=1"), + // Depends on RabbitMQ management being configured on the backend - don't let it take the + // rest of the dashboard down when it isn't; the "Queue alerts" tile flags it instead. + get("/ops/alerts").catch(() => null), + subscriptionMethods.listSubscriptionRows(), + ]); const subscriptionNameById = new Map(subscriptionRows.map((i) => [i.id, i.name])); const latestFailures = [...failed.result, ...badResponses.result] @@ -89,10 +83,6 @@ export const dashboardMethods = { })); return { - inFlight: inFlight.totalCount, - failuresToActOn: failuresRes.totalCount, - failingChains: retrySummary.failingChains ?? [], - retriesLast7Days: retrySummary.retriesLast7Days ?? { finished: 0, succeeded: 0 }, pendingRetries: delayedRes.totalCount, queueAlerts: alertsRaw === null ? null : alertsRaw.filter((a) => a.severity !== "Info").length, latestFailures, @@ -104,4 +94,30 @@ export const dashboardMethods = { }, }; }, + + /** + * The problems still to deal with, which the tile's list opens. Counted by the server from the + * failures: the list fetches rows newest first, which for this filter can mean reading back + * through millions of exchanges first. + */ + async getFailuresToActOn(): Promise { + return (await get<{ total: number }>("/dashboard/failurestoacton")).total; + }, + + async getInFlight(): Promise { + const lastDay = startedSince(86_400_000); + return ( + await get>( + `/xchanges?filter=${lastDay}&filter=${encodeURIComponent("StatusFilter:1:0")}&size=1`, + ) + ).totalCount; + }, + + async getRetrySummary(): Promise { + const raw = await get("/dashboard/retrysummary"); + return { + failingChains: raw.failingChains ?? [], + retriesLast7Days: raw.retriesLast7Days ?? { finished: 0, succeeded: 0 }, + }; + }, } satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/queryKeys.ts b/SW.Bitween.Web/ClientApp/src/api/queryKeys.ts index 004830ba..8467396f 100644 --- a/SW.Bitween.Web/ClientApp/src/api/queryKeys.ts +++ b/SW.Bitween.Web/ClientApp/src/api/queryKeys.ts @@ -201,6 +201,10 @@ export const keys = { categories: ["subscription-categories"] as const, background: ["queue-health", "background"] as const, dashboard: ["dashboard"] as const, + // Under the dashboard's own key, so whatever refreshes the dashboard refreshes these too. + dashboardFailuresToActOn: ["dashboard", "failures-to-act-on"] as const, + dashboardInFlight: ["dashboard", "in-flight"] as const, + dashboardRetrySummary: ["dashboard", "retry-summary"] as const, metrics: { all: ["metrics"] as const, overview: ["metrics", "overview"] as const, diff --git a/SW.Bitween.Web/ClientApp/src/api/types.ts b/SW.Bitween.Web/ClientApp/src/api/types.ts index 04528063..ca6077b7 100644 --- a/SW.Bitween.Web/ClientApp/src/api/types.ts +++ b/SW.Bitween.Web/ClientApp/src/api/types.ts @@ -1837,8 +1837,12 @@ export interface FailingChain { exception: string | null; } -export interface DashboardData { - /** Worst first. Empty when nothing has been retried more than once and left failing. */ +/** The dashboard's retry panel. */ +export interface RetrySummary { + /** + * Worst first, among chains whose newest attempt started in the last 7 days. Empty when nothing + * has been retried more than once and left failing. + */ failingChains: FailingChain[]; /** * Retries started in the last 7 days: how many have finished, and how many of those worked. @@ -1846,14 +1850,9 @@ export interface DashboardData { * luck or a configuration problem. */ retriesLast7Days: { finished: number; succeeded: number }; - /** - * Failures that are the newest attempt of their chain. Counts problems rather than attempts: a - * chain retried nine times is one of these, not nine. Subject to the search's count cap, so a - * value above it means "at least this many". - */ - failuresToActOn: number; - /** Exchanges started in the last 24 hours and not finished yet. */ - inFlight: number; +} + +export interface DashboardData { pendingRetries: number; /** Null when the RabbitMQ management API is unavailable - never counts as zero. */ queueAlerts: number | null; diff --git a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx index dc7055e6..dc7d8739 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx @@ -116,6 +116,27 @@ export function DashboardPage() { placeholderData: keepPreviousData, }); const insights = useQuery({ queryKey: keys.metrics.insights, queryFn: () => api.getInsights() }); + // Counted over the exchange tables, so asked for apart: a slow one fills in on its own tile + // rather than holding up the page. Not retried - the next minute's refresh asks again, rather than + // three retries piling onto a database already struggling with it. + const failuresToActOn = useQuery({ + queryKey: keys.dashboardFailuresToActOn, + queryFn: () => api.getFailuresToActOn(), + refetchInterval: 60_000, + retry: false, + }); + const inFlight = useQuery({ + queryKey: keys.dashboardInFlight, + queryFn: () => api.getInFlight(), + refetchInterval: 60_000, + retry: false, + }); + const retrySummary = useQuery({ + queryKey: keys.dashboardRetrySummary, + queryFn: () => api.getRetrySummary(), + refetchInterval: 60_000, + retry: false, + }); const data = dashboard.data; const o = metrics.data; @@ -172,7 +193,7 @@ export function DashboardPage() { <> {delta >= 0 ? "+" : ""} {compact(delta)} vs the day before - {data.inFlight > 0 && <> · {compact(data.inFlight)} in flight} + {inFlight.data !== undefined && inFlight.data > 0 && <> · {compact(inFlight.data)} in flight} } to="/exchanges" @@ -205,11 +226,25 @@ export function DashboardPage() { is the current state of anything. This counts the ends of chains: the problems. */} 10_000 ? "10,000+" : whole(data.failuresToActOn)} - sub={data.failuresToActOn > 0 ? "newest attempt of each chain" : "every failure was retried"} + // The count stops past its cap and reports the cap + 1. + value={ + failuresToActOn.data === undefined + ? "-" + : failuresToActOn.data > 10_000 + ? "10,000+" + : whole(failuresToActOn.data) + } + sub={ + failuresToActOn.data === undefined + ? failuresToActOn.isError + ? "unavailable right now" + : "counting…" + : failuresToActOn.data > 0 + ? "newest attempt of each chain" + : "every failure was retried" + } to="/exchanges?status=failed&latest=1" - accent={data.failuresToActOn > 0 ? "danger" : undefined} + accent={failuresToActOn.data !== undefined && failuresToActOn.data > 0 ? "danger" : undefined} title="Failures that nothing has been retried from yet. A chain of nine failed attempts counts once - as the attempt at the end of it, which is the one still to deal with." /> - {data.failingChains.length === 0 ? ( + {!retrySummary.data ? ( + retrySummary.isError ? ( + + The retry chains couldn't be read in time. The dashboard tries again every minute. + + ) : ( + + ) + ) : retrySummary.data.failingChains.length === 0 ? ( Nothing has been retried more than once and left failing. ) : (
    - {data.failingChains.map((c) => ( + {retrySummary.data.failingChains.map((c) => (
  • [], metrics = m http.get(apiPath("/dashboard/retrysummary"), () => HttpResponse.json({ retriesLast7Days: { finished: 0, succeeded: 0 }, failingChains: [] }), ), + http.get(apiPath("/dashboard/failurestoacton"), () => HttpResponse.json({ total: 0 })), http.get(apiPath("/ops/alerts"), () => HttpResponse.json([])), http.get(apiPath("/subscriptions"), () => HttpResponse.json({ result: subscriptions, totalCount: subscriptions.length }), @@ -123,6 +124,44 @@ describe("the dashboard", () => { expect(screen.queryByRole("heading", { name: "Getting started" })).not.toBeInTheDocument(); }); + it("shows a count that couldn't be read in time on its own tile, and the rest of the dashboard anyway", async () => { + renderApp("/dashboard", { + handlers: [ + // A database timeout reaches the browser as a 500. + http.get(apiPath("/dashboard/failurestoacton"), () => new HttpResponse(null, { status: 500 })), + http.get(apiPath("/dashboard/retrysummary"), () => new HttpResponse(null, { status: 500 })), + ...dashboard([subscription(0, { failures: 2 })]), + ], + }); + + const tile = (await screen.findByText("Failures to act on")).closest("a")!; + expect(within(tile).getByText("-")).toBeVisible(); + expect(within(tile).getByText("unavailable right now")).toBeVisible(); + + const chains = within(screen.getByRole("heading", { name: "Chains that keep failing" }).closest("section")!); + expect(chains.getByText("Unavailable right now")).toBeVisible(); + + const health = within(screen.getByRole("heading", { name: "Subscription health" }).closest("section")!); + expect(health.getByText("Health page 1", { exact: true })).toBeVisible(); + }); + + it("doesn't wait for a slow count before showing the rest", async () => { + renderApp("/dashboard", { + handlers: [ + http.get(apiPath("/dashboard/failurestoacton"), async () => { + await delay("infinite"); + return HttpResponse.json({ total: 0 }); + }), + ...dashboard([subscription(0, { failures: 2 })]), + ], + }); + + const health = within((await screen.findByRole("heading", { name: "Subscription health" })).closest("section")!); + expect(health.getByText("Health page 1", { exact: true })).toBeVisible(); + const tile = screen.getByText("Failures to act on").closest("a")!; + expect(within(tile).getByText("counting…")).toBeVisible(); + }); + it("reads its numbers from the metrics, and names subscriptions that have gone quiet", async () => { const metrics = overview({ since: "2026-09-01T00:00:00Z", diff --git a/docs/api-reference.md b/docs/api-reference.md index 054dc612..2a788078 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -389,5 +389,6 @@ See [Data sources](data-sources.md) and [Databases](databases.md). | `GET /api/dashboard/MainInfo` | `dashboard.view` | | `GET /api/dashboard/ChartsDataPoints` | `dashboard.view` | | `GET /api/dashboard/XChangesAndSubscriptionsInfo` | `dashboard.view` | -| `GET /api/dashboard/retrysummary` | `dashboard.view`. Retries finished in the last 7 days, and the five failing chains with the most attempts. | +| `GET /api/dashboard/retrysummary` | `dashboard.view`. Retries finished in the last 7 days, and the five failing chains with the most attempts among those retried in the same 7 days. | +| `GET /api/dashboard/failurestoacton` | `dashboard.view`. `{ total }`: failed exchanges nothing has been retried from, the count the exchange search gives for `StatusFilter:1:3` with `LatestOnly:1:true`. Capped like the search: past 10,000 it reports 10,001. | | `GET /health` | none | diff --git a/docs/operations.md b/docs/operations.md index d3c683a5..26cb7032 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -71,7 +71,7 @@ See [Scheduling](scheduling.md). The dashboard, opened from the logo, shows today's exchanges, the 7-day success rate, today's failures, waiting retries, queue alerts, a 14-day chart, the latest failures, unhealthy subscriptions and the busiest subscriptions. The browser computes it from up to 1,000 recent exchanges, so figures on a busy instance are approximate. -Two parts come from the server instead. **Failures to act on** counts failed exchanges that are the newest attempt of their chain, of any age, and links to that search. **Chains that keep failing** lists the five retry chains with the most attempts, with the success rate of retries over the last 7 days. +Two parts come from the server instead. **Failures to act on** counts failed exchanges that are the newest attempt of their chain, of any age, and links to that search. **Chains that keep failing** lists the five retry chains with the most attempts among those retried in the last 7 days, with the success rate of retries over the same 7 days. A part that can't be read in time shows as unavailable while the rest of the dashboard loads. ## Audit trail From 9a0d91f6006023c8a463cba3618c8bdcc3f87eb3 Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Sun, 1 Nov 2026 16:48:12 +0300 Subject: [PATCH 2/2] fix: dashboard timeouts review findings --- SW.Bitween.PgSql/ConcurrentIndex.cs | 14 +++++++++++--- .../src/pages/dashboard/DashboardPage.tsx | 1 + .../dashboard/__tests__/DashboardPage.test.tsx | 13 +++++++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/SW.Bitween.PgSql/ConcurrentIndex.cs b/SW.Bitween.PgSql/ConcurrentIndex.cs index 2ac6eab3..e46995a7 100644 --- a/SW.Bitween.PgSql/ConcurrentIndex.cs +++ b/SW.Bitween.PgSql/ConcurrentIndex.cs @@ -10,14 +10,20 @@ namespace SW.Bitween.PgSql; /// table of tens of millions of rows, inside the startup migration. CONCURRENTLY doesn't, but it /// cannot run in a transaction, and a build that fails part-way leaves the index behind marked /// invalid: the IF NOT EXISTS would then skip the rebuild and the migration would pass with an -/// index Postgres never uses. So an invalid leftover is dropped first, as -/// PromotedPropertiesTrigramIndex does. +/// index Postgres never uses. So an invalid leftover goes first. +/// +/// Not with a plain DROP INDEX, which locks the whole table: queued behind whatever is reading it, +/// it holds up every read and insert that comes after. The leftover is renamed out of the way +/// instead - a lock reads and writes don't wait for - and dropped concurrently, which a DO block +/// can't do. A valid index of the name is left as it is. /// internal static class ConcurrentIndex { /// What follows ON infolink.table: the columns, and any WHERE. public static void Create(MigrationBuilder migrationBuilder, string table, string name, string definition) { + var leftover = name + "_invalid"; + migrationBuilder.Sql( $@"DO $$ BEGIN @@ -28,10 +34,12 @@ FROM pg_index i JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '{BitweenDbContext.Schema}' AND c.relname = '{name}' AND NOT i.indisvalid ) THEN - DROP INDEX {BitweenDbContext.Schema}.{name}; + ALTER INDEX {BitweenDbContext.Schema}.{name} RENAME TO {leftover}; END IF; END $$;"); + Drop(migrationBuilder, leftover); + migrationBuilder.Sql( $"CREATE INDEX CONCURRENTLY IF NOT EXISTS {name} ON {BitweenDbContext.Schema}.{table} {definition};", suppressTransaction: true); diff --git a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx index dc7d8739..5c098773 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx @@ -194,6 +194,7 @@ export function DashboardPage() { {delta >= 0 ? "+" : ""} {compact(delta)} vs the day before {inFlight.data !== undefined && inFlight.data > 0 && <> · {compact(inFlight.data)} in flight} + {inFlight.isError && inFlight.data === undefined && <> · in flight unavailable} } to="/exchanges" diff --git a/SW.Bitween.Web/ClientApp/src/pages/dashboard/__tests__/DashboardPage.test.tsx b/SW.Bitween.Web/ClientApp/src/pages/dashboard/__tests__/DashboardPage.test.tsx index 63b84093..e2cff861 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/dashboard/__tests__/DashboardPage.test.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/dashboard/__tests__/DashboardPage.test.tsx @@ -130,16 +130,25 @@ describe("the dashboard", () => { // A database timeout reaches the browser as a 500. http.get(apiPath("/dashboard/failurestoacton"), () => new HttpResponse(null, { status: 500 })), http.get(apiPath("/dashboard/retrysummary"), () => new HttpResponse(null, { status: 500 })), + http.get(apiPath("/xchanges"), ({ request }) => + new URL(request.url).searchParams.getAll("filter").includes("StatusFilter:1:0") + ? new HttpResponse(null, { status: 500 }) + : HttpResponse.json(none), + ), ...dashboard([subscription(0, { failures: 2 })]), ], }); const tile = (await screen.findByText("Failures to act on")).closest("a")!; + expect(await within(tile).findByText("unavailable right now")).toBeVisible(); expect(within(tile).getByText("-")).toBeVisible(); - expect(within(tile).getByText("unavailable right now")).toBeVisible(); + + // Not left looking like nothing is in flight. + const exchanges = screen.getByText("Exchanges, last 24 hours").closest("a")!; + expect(await within(exchanges).findByText(/in flight unavailable/)).toBeVisible(); const chains = within(screen.getByRole("heading", { name: "Chains that keep failing" }).closest("section")!); - expect(chains.getByText("Unavailable right now")).toBeVisible(); + expect(await chains.findByText("Unavailable right now")).toBeVisible(); const health = within(screen.getByRole("heading", { name: "Subscription health" }).closest("section")!); expect(health.getByText("Health page 1", { exact: true })).toBeVisible();