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/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.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