diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1a5b3a71..a8f4d5cc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,7 +13,7 @@ updates: timezone: "Asia/Amman" cooldown: default-days: 3 - semver-patch-days: 1 + semver-patch-days: 5 semver-minor-days: 3 semver-major-days: 7 open-pull-requests-limit: 10 @@ -36,7 +36,7 @@ updates: timezone: "Asia/Amman" cooldown: default-days: 3 - semver-patch-days: 1 + semver-patch-days: 5 semver-minor-days: 3 semver-major-days: 7 open-pull-requests-limit: 5 @@ -54,7 +54,7 @@ updates: timezone: "Asia/Amman" cooldown: default-days: 3 - semver-patch-days: 1 + semver-patch-days: 5 semver-minor-days: 3 semver-major-days: 7 open-pull-requests-limit: 5 diff --git a/.gitignore b/.gitignore index ee53b660..8b370d98 100644 --- a/.gitignore +++ b/.gitignore @@ -365,3 +365,15 @@ SW.Bitween.NativeAdapters/JsonFieldMapper/Bitween-api.code-workspace .vscode/ .postman/ postman/ + +# Admin UI build output (generated by ClientApp `yarn build` into wwwroot) +SW.Bitween.Web/wwwroot/index.html +SW.Bitween.Web/wwwroot/assets/ +SW.Bitween.Web/wwwroot/brand/ +SW.Bitween.Web/wwwroot/favicon.svg +SW.Bitween.Web/wwwroot/icons.svg + +# Playwright MCP tool output and test-run artifacts (created at repo root when +# run from here, separate from ClientApp's own .gitignore'd copies) +/.playwright-mcp/ +/test-results/ diff --git a/Dockerfile b/Dockerfile index 63224453..676c18a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base COPY --from=mcr.microsoft.com/dotnet/aspnet:6.0 /usr/share/dotnet/shared /usr/share/dotnet/shared @@ -8,7 +8,15 @@ WORKDIR /app EXPOSE 8080 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +# Build the admin UI (SPA) in its own stage; output lands in SW.Bitween.Web/wwwroot +FROM node:22-alpine AS ui-build +WORKDIR /src/SW.Bitween.Web/ClientApp +COPY ["SW.Bitween.Web/ClientApp/package.json", "SW.Bitween.Web/ClientApp/yarn.lock", "./"] +RUN yarn install --frozen-lockfile +COPY ["SW.Bitween.Web/ClientApp/", "./"] +RUN yarn build + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["SW.Bitween.Web/SW.Bitween.Web.csproj", "SW.Bitween.Web/"] COPY ["SW.Bitween.Api/SW.Bitween.Api.csproj", "SW.Bitween.Api/"] @@ -21,9 +29,11 @@ WORKDIR "/src/SW.Bitween.Web" RUN dotnet build "SW.Bitween.Web.csproj" -c Release -o /app/build FROM build AS publish -RUN dotnet publish "SW.Bitween.Web.csproj" -c Release -o /app/publish +# UI is built in the ui-build stage; the SDK image has no node +RUN dotnet publish "SW.Bitween.Web.csproj" -c Release -o /app/publish -p:SkipClientBuild=true FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "SW.Bitween.Web.dll"] \ No newline at end of file +COPY --from=ui-build /src/SW.Bitween.Web/wwwroot ./wwwroot +ENTRYPOINT ["dotnet", "SW.Bitween.Web.dll"] diff --git a/SW.Bitween.Api/Controllers/GatewayController.cs b/SW.Bitween.Api/Controllers/GatewayController.cs index 43973b87..a4372580 100644 --- a/SW.Bitween.Api/Controllers/GatewayController.cs +++ b/SW.Bitween.Api/Controllers/GatewayController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Mime; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; @@ -56,6 +57,17 @@ private async Task ProcessAsync([FromRoute] string gatewayApiName if (apiGatewayPartner == null) return Unauthorized(); + // After authorisation on purpose: whether a gateway exists and is switched off is + // something only an attached partner should learn — checking it earlier would + // answer that for anyone who guessed the url. + // + // 503 rather than 404 because the url is right and the partner should keep it: a + // 404 reads as "wrong address" and sends someone hunting for a new one, where this + // is a gateway somebody switched off and will switch back on. + if (apiGateway.Inactive) + return StatusCode(StatusCodes.Status503ServiceUnavailable, + $"The '{apiGateway.Name}' gateway is currently deactivated."); + var subscription = await cache.SubscriptionByIdAsync(apiGatewayPartner.SubscriptionId); if (subscription == null) @@ -63,7 +75,7 @@ private async Task ProcessAsync([FromRoute] string gatewayApiName var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); - var xchangeFile = new XchangeFile(json); + var xchangeFile = new XchangeFile(json, $"{gatewayApiName}.json"); var validatorProperties = subscription.ValidatorProperties.ToDictionary() .Fill(partner, globalAdapterValuesSet); diff --git a/SW.Bitween.Api/Data/BitweenDbContext.cs b/SW.Bitween.Api/Data/BitweenDbContext.cs index cadd4704..1a3f0eae 100644 --- a/SW.Bitween.Api/Data/BitweenDbContext.cs +++ b/SW.Bitween.Api/Data/BitweenDbContext.cs @@ -41,12 +41,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(b => { b.ToTable("Documents"); - b.Property(p => p.Id).ValueGeneratedNever(); b.Property(p => p.Name).HasMaxLength(100).IsUnicode(false).IsRequired(); + b.Property(p => p.Code).HasMaxLength(50).IsUnicode(false); b.Property(p => p.BusMessageTypeName).IsUnicode(false).HasMaxLength(500); b.Property(p => p.PromotedProperties).StoreAsJson(); b.Property(p => p.DisregardsUnfilteredMessages).IsRequired(false); b.HasIndex(p => p.Name).IsUnique(); + b.HasIndex(p => p.Code).IsUnique(); b.HasIndex(p => p.BusMessageTypeName).IsUnique(); b.HasMany().WithOne().HasForeignKey(p => p.DocumentId).OnDelete(DeleteBehavior.Restrict); @@ -228,6 +229,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(p => p.Name).IsRequired().HasMaxLength(200); b.Property(p => p.Groups).StoreAsJson(); + b.Property(p => p.AlertHandlerId).HasMaxLength(200).IsUnicode(false); + b.Property(p => p.AlertHandlerProperties).StoreAsJson(); }); modelBuilder.Entity(b => @@ -236,10 +239,36 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasKey(p => p.Id); b.Property(p => p.Id).IsUnicode(false).HasMaxLength(50); b.Property(p => p.On); - b.Property(p => p.GroupAttemptCounts).StoreAsJson(); b.HasIndex(p => p.On); }); + modelBuilder.Entity(b => + { + b.ToTable("ReceiveAttempts"); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.ErrorMessage).HasMaxLength(4000); + b.Property(p => p.ExchangeIds).IsSeparatorDelimited(); + b.HasIndex(p => new { p.SubscriptionId, p.StartedOn }); + }); + + modelBuilder.Entity(b => + { + b.ToTable("RetryGroupUsages"); + b.HasKey(p => new { p.SubscriptionId, p.GroupId }); + b.Property(p => p.AttemptsUsed); + b.Property(p => p.LastAttemptOn); + b.Property(p => p.ExhaustedNotifiedOn); + }); + + modelBuilder.Entity(b => + { + b.ToTable("RetryAlertOverrides"); + b.HasKey(p => new { p.SubscriptionId, p.GroupId }); + b.Property(p => p.AlertMode).HasConversion(); + b.Property(p => p.AlertHandlerId).HasMaxLength(200).IsUnicode(false); + b.Property(p => p.AlertHandlerProperties).StoreAsJson(); + }); + modelBuilder.Entity(b => { b.ToTable("Xchanges"); @@ -252,7 +281,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.HandlerId).HasMaxLength(200).IsUnicode(false); b.Property(p => p.HandlerProperties).StoreAsJson(); b.Property(p => p.MapperProperties).StoreAsJson(); - b.Property(p => p.GroupAttemptCounts).StoreAsJson(); b.Property(p => p.InputContentType).IsUnicode(false).HasMaxLength(200); b.Property(p => p.ResponseMessageTypeName).IsUnicode(false).HasMaxLength(500); @@ -283,6 +311,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.ResponseName).HasMaxLength(200); b.Property(p => p.ResponseContentType).IsUnicode(false).HasMaxLength(200); b.Property(p => p.OutputContentType).IsUnicode(false).HasMaxLength(200); + b.Property(p => p.RetryBlockedReason).HasMaxLength(500); + b.Property(p => p.RetryGroupId); + b.Property(p => p.AttemptNumber); + b.HasIndex(p => p.RetryGroupId); b.HasOne().WithOne().HasForeignKey(p => p.Id).OnDelete(DeleteBehavior.Cascade); @@ -349,7 +381,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasIndex(p => p.Email).IsUnique(); b.Property(p => p.Email).IsUnicode(false).HasMaxLength(200); - b.Property(p => p.Phone).IsUnicode(false).HasMaxLength(20); b.Property(p => p.Password).IsUnicode(false).HasMaxLength(500); b.Property(p => p.DisplayName).IsRequired().HasMaxLength(200); @@ -368,7 +399,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Disabled = false, Password = defaultPasswordHash, Deleted = false, - Role = AccountRole.Admin + Role = AccountRole.Admin, + FailedLoginCount = 0 }); }); @@ -382,9 +414,57 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.AccountId); b.Property(p => p.LoginMethod).HasConversion(); }); - + + modelBuilder.Entity(b => + { + b.ToTable("Roles"); + b.HasKey(p => p.Id); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.HasIndex(p => p.Name).IsUnique(); + + b.Property(p => p.Name).IsRequired().HasMaxLength(100); + b.Property(p => p.Description).HasMaxLength(500); + b.Property(p => p.Permissions).StoreAsJson(); + + b.HasData(SystemRoleSeed()); + }); + + modelBuilder.Entity(b => + { + b.ToTable("AccountRoles"); + b.HasKey(p => new { p.AccountId, p.RoleId }); + b.HasOne().WithMany().HasForeignKey(p => p.AccountId).OnDelete(DeleteBehavior.Cascade); + b.HasOne().WithMany().HasForeignKey(p => p.RoleId).OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(b => + { + b.ToTable("Settings"); + b.HasKey(p => p.Id); + // Id is the catalog key, e.g. "Theme.PrimaryColor". Value is left unbounded: + // it carries anything from a hex color to a license key or a page of blurb. + b.Property(p => p.Id).IsUnicode(false).HasMaxLength(200); + }); + } + /// + /// The built-in roles. Their grants aren't stored — + /// derives them from the catalog — so adding a permission never needs a data fix-up here. + /// + protected Role[] SystemRoleSeed() => + [ + new Role(Role.AdministratorId, "Administrator", + "Full access to everything, including members, roles and settings.") + { CreatedOn = defaultCreatedOn.ToUniversalTime() }, + new Role(Role.MemberId, "Member", + "Runs and configures integrations. Can't manage members, roles or settings.") + { CreatedOn = defaultCreatedOn.ToUniversalTime() }, + new Role(Role.ViewerId, "Viewer", + "Read-only access to integrations, exchanges and configuration.") + { CreatedOn = defaultCreatedOn.ToUniversalTime() } + ]; + async public override Task SaveChangesAsync(CancellationToken cancellationToken = default) { ChangeTracker.ApplyAuditValues(requestContext.GetNameIdentifier()); diff --git a/SW.Bitween.Api/Domain/Accounts/Account.cs b/SW.Bitween.Api/Domain/Accounts/Account.cs index a2bb0d0b..01e3b2ce 100644 --- a/SW.Bitween.Api/Domain/Accounts/Account.cs +++ b/SW.Bitween.Api/Domain/Accounts/Account.cs @@ -18,7 +18,6 @@ public Account(string displayName, string email, string password, AccountRole ro } public string Email { get; private set; } - public string Phone { get; private set; } public string DisplayName { get; set; } public AccountRole Role { get; private set; } @@ -29,6 +28,24 @@ public Account(string displayName, string email, string password, AccountRole ro public string Password { get; set; } + public int FailedLoginCount { get; private set; } + public DateTime? LockoutEnd { get; private set; } + + public bool IsLockedOut(DateTime nowUtc) => LockoutEnd.HasValue && LockoutEnd.Value > nowUtc; + + public void RegisterSuccessfulLogin() + { + FailedLoginCount = 0; + LockoutEnd = null; + } + + // Admin action: clear a lockout before it expires. + public void Unlock() + { + FailedLoginCount = 0; + LockoutEnd = null; + } + public bool AddEmailLoginMethod(string email, string password) { @@ -63,6 +80,26 @@ public void Update(string name, AccountRole role) Role = role; } + /// Self-service details. Deliberately cannot touch . + public void UpdateProfile(string name) + { + DisplayName = name; + } + + /// + /// Legacy coarse role, kept in step with the account's roles for older API consumers. + /// Authorization itself reads the role assignments, not this. + /// + public void SetRole(AccountRole role) + { + Role = role; + } + + public void SetDisabled(bool disabled) + { + Disabled = disabled; + } + public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } public DateTime? ModifiedOn { get; set; } diff --git a/SW.Bitween.Api/Domain/Accounts/AccountRoleLink.cs b/SW.Bitween.Api/Domain/Accounts/AccountRoleLink.cs new file mode 100644 index 00000000..acc1047a --- /dev/null +++ b/SW.Bitween.Api/Domain/Accounts/AccountRoleLink.cs @@ -0,0 +1,18 @@ +namespace SW.Bitween.Domain.Accounts; + +/// Which roles an account holds. Composite key — one row per account/role pair. +public class AccountRoleLink +{ + private AccountRoleLink() + { + } + + public AccountRoleLink(int accountId, int roleId) + { + AccountId = accountId; + RoleId = roleId; + } + + public int AccountId { get; private set; } + public int RoleId { get; private set; } +} diff --git a/SW.Bitween.Api/Domain/Accounts/Role.cs b/SW.Bitween.Api/Domain/Accounts/Role.cs new file mode 100644 index 00000000..99db343e --- /dev/null +++ b/SW.Bitween.Api/Domain/Accounts/Role.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain.Accounts; + +/// +/// A reusable set of permission keys. Members hold any number of roles and may do anything +/// any of their roles allows. +/// +public class Role : BaseEntity, IAudited +{ + public const int AdministratorId = 1; + public const int MemberId = 2; + public const int ViewerId = 3; + + /// The groups the built-in Member and Viewer roles reach; Administration stays admin-only. + private static readonly string[] NonAdminGroups = ["Operate", "Integrations", "Configuration"]; + + private Role() + { + } + + /// Seeding path for the built-in roles — fixed Id, grants computed at runtime. + public Role(int id, string name, string description) + { + Id = id; + Name = name; + Description = description; + IsSystem = true; + } + + public Role(string name, string description, IEnumerable permissions) + { + Name = name; + Description = description; + Permissions = PermissionCatalog.Sanitize(permissions); + } + + public string Name { get; private set; } + public string Description { get; private set; } + + /// + /// Catalog keys, stored as one JSON column. Empty for built-in roles — see + /// . + /// + public List Permissions { get; private set; } = []; + + /// Built-in roles can be assigned, but never edited or deleted. + public bool IsSystem { get; private set; } + + public void Update(string name, string description, IEnumerable permissions) + { + Name = name; + Description = description; + Permissions = PermissionCatalog.Sanitize(permissions); + } + + /// + /// What this role actually grants. Built-in roles derive their grants from the catalog at + /// runtime instead of storing them, so adding a permission to the catalog reaches + /// Administrators — and the right groups — without a migration or a data fix-up. + /// + public List GetEffectivePermissions() => IsSystem ? SystemPermissions(Id) : Permissions; + + public static List SystemPermissions(int roleId) => roleId switch + { + AdministratorId => PermissionCatalog.AllKeys.ToList(), + MemberId => PermissionCatalog.InGroups(false, NonAdminGroups), + ViewerId => PermissionCatalog.InGroups(true, NonAdminGroups), + _ => [] + }; + + public DateTime CreatedOn { get; set; } + public string CreatedBy { get; set; } + public DateTime? ModifiedOn { get; set; } + public string ModifiedBy { get; set; } +} diff --git a/SW.Bitween.Api/Domain/DelayedRetry.cs b/SW.Bitween.Api/Domain/DelayedRetry.cs index c744320a..c8bd1ffc 100644 --- a/SW.Bitween.Api/Domain/DelayedRetry.cs +++ b/SW.Bitween.Api/Domain/DelayedRetry.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using SW.PrimitiveTypes; namespace SW.Bitween.Domain; @@ -7,5 +6,4 @@ namespace SW.Bitween.Domain; public class DelayedRetry : BaseEntity { public DateTime On { get; set; } - public Dictionary GroupAttemptCounts { get; set; } = new(); } diff --git a/SW.Bitween.Api/Domain/Document/Document.cs b/SW.Bitween.Api/Domain/Document/Document.cs index c9e4147b..e12a33eb 100644 --- a/SW.Bitween.Api/Domain/Document/Document.cs +++ b/SW.Bitween.Api/Domain/Document/Document.cs @@ -24,7 +24,17 @@ public Document(int id, string name, DocumentFormat format) DocumentFormat = format; } + /// Production creation path — Id is database-generated. Code is optional. + public Document(string code, string name, DocumentFormat format) + { + Code = code; + Name = name ?? throw new ArgumentNullException(nameof(name)); + PromotedProperties = new Dictionary(); + DocumentFormat = format; + } + public string Name { get; private set; } + public string Code { get; private set; } public bool BusEnabled { get; set; } public string BusMessageTypeName { get; set; } public int DuplicateInterval { get; set; } @@ -37,5 +47,15 @@ public void SetDictionaries(IReadOnlyDictionary promotedProperti { PromotedProperties = promotedProperties; } + + public void SetName(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + public void SetCode(string code) + { + Code = code; + } } } \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs b/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs index 7f304f7f..4f1aef35 100644 --- a/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs +++ b/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs @@ -8,6 +8,13 @@ public class ApiGateway : BaseEntity,IAudited { public string Name { get; set; } public string UrlName { get; set; } + + /// + /// Turns the gateway off without deleting it. Deleting is the only alternative today, + /// and it takes the partner attachments with it — so a gateway that needs stopping for + /// an afternoon gets rebuilt by hand afterwards, or left running. + /// + public bool Inactive { get; set; } public ICollection Partners { get; set; } public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } diff --git a/SW.Bitween.Api/Domain/Gateway/BusGateway.cs b/SW.Bitween.Api/Domain/Gateway/BusGateway.cs index d2c83d41..0e61eaa5 100644 --- a/SW.Bitween.Api/Domain/Gateway/BusGateway.cs +++ b/SW.Bitween.Api/Domain/Gateway/BusGateway.cs @@ -8,6 +8,12 @@ public class BusGateway : BaseEntity, IAudited { public string Name { get; set; } public int DocumentId { get; set; } + + /// + /// Turns the gateway off without deleting it — its routes stop being offered the + /// message. See . + /// + public bool Inactive { get; set; } public ICollection Routes { get; set; } public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } diff --git a/SW.Bitween.Api/Domain/ReceiveAttempt.cs b/SW.Bitween.Api/Domain/ReceiveAttempt.cs new file mode 100644 index 00000000..a7ceaa54 --- /dev/null +++ b/SW.Bitween.Api/Domain/ReceiveAttempt.cs @@ -0,0 +1,20 @@ +using System; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; + +/// +/// One execution of a Receiving subscription's receive step, written by ReceivingJob +/// itself right where it already catches the receiver's own failures — kept independent of +/// Quartz's own run history and unaffected by how Quartz treats a thrown exception. +/// +public class ReceiveAttempt : BaseEntity +{ + public int SubscriptionId { get; set; } + public DateTime StartedOn { get; set; } + public DateTime FinishedOn { get; set; } + public ReceiveOutcome Outcome { get; set; } + public string ErrorMessage { get; set; } + public string[] ExchangeIds { get; set; } = Array.Empty(); +} diff --git a/SW.Bitween.Api/Domain/RetryAlertOverride.cs b/SW.Bitween.Api/Domain/RetryAlertOverride.cs new file mode 100644 index 00000000..61d1b6b9 --- /dev/null +++ b/SW.Bitween.Api/Domain/RetryAlertOverride.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using SW.Bitween.Model; + +namespace SW.Bitween.Domain; + +/// +/// The most specific level of the retry-alert hierarchy: where one subscription's failures in one +/// retry group should be alerted, overriding whatever the group or the policy says. +/// +/// +/// Deliberately its own table rather than columns on . Usage rows are +/// deleted by RetryPolicies/resetusage, so config stored there would be silently discarded +/// every time someone cleared a spent budget. +/// +public class RetryAlertOverride +{ + /// The subscription this override applies to. + public int SubscriptionId { get; set; } + + /// RetryGroup.Id, which survives policy edits, so the override does too. + public Guid GroupId { get; set; } + + /// + /// Whether this level sends, stays silent, or defers upward. A row whose mode is + /// is equivalent to having no row at all. + /// + public RetryAlertMode AlertMode { get; set; } + + /// Adapter that delivers the alert. Required when is Send. + public string AlertHandlerId { get; set; } + + /// That adapter's own settings — api key, recipients, subject. + public IReadOnlyDictionary AlertHandlerProperties { get; set; } +} diff --git a/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs b/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs new file mode 100644 index 00000000..b5f7e29f --- /dev/null +++ b/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs @@ -0,0 +1,42 @@ +using System; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; + +/// +/// Raised the one time a retry group's MaxAttemptsTotal runs out for a subscription, so the +/// configured alert handler can be told that failures matching that group have stopped being retried. +/// +/// +/// +/// Deliberately not an IHasWorkGroup event: it publishes under its own type name and is picked +/// up by a dedicated IConsume<RetryBudgetExhaustedEvent> consumer with its own queue. A +/// slow or broken alert handler therefore cannot delay or fail the ordinary notifier path, which +/// shares the work group's result queue. +/// +/// +/// Carried on rather than published directly, so it only reaches the bus +/// once the failure it describes has actually been committed. +/// +/// +public class RetryBudgetExhaustedEvent : BaseDomainEvent +{ + /// The failure that found the budget empty. + public string XchangeId { get; set; } + + public int SubscriptionId { get; set; } + + public Guid GroupId { get; set; } + + /// The group's name as it was when the budget ran out, in case it is later renamed. + public string GroupName { get; set; } + + /// + /// The ceiling that was reached. Not paired with an "used" count, because at exhaustion the two + /// are the same number — except when the ceiling was lowered below what had already been spent, + /// where the ceiling is still the meaningful figure. + /// + public int MaxAttemptsTotal { get; set; } + + public DateTime OccurredOn { get; set; } +} diff --git a/SW.Bitween.Api/Domain/RetryGroupUsage.cs b/SW.Bitween.Api/Domain/RetryGroupUsage.cs new file mode 100644 index 00000000..b19f7a9b --- /dev/null +++ b/SW.Bitween.Api/Domain/RetryGroupUsage.cs @@ -0,0 +1,41 @@ +using System; + +namespace SW.Bitween.Domain; + +/// +/// Running total of the retries one retry group has spent for one integration, backing +/// RetryBudget.MaxAttemptsTotal. That cap is shared by every message hitting the +/// group, so it cannot be tracked on an individual xchange. +/// +/// +/// Once reaches the group's MaxAttemptsTotal the group stops +/// retrying for that integration until this row is cleared. A row that has reached the cap is cleared +/// by the integration's next success — the only signal that the downstream it was failing against has +/// recovered — or by one of the reset endpoints. A row still below the cap is left alone by a success: +/// the cap is there for a downstream that fails some messages and succeeds others, which is exactly +/// when crediting it back would stop it ever being reached. +/// +public class RetryGroupUsage +{ + /// The integration whose budget this is. A shared policy gives each one its own total. + public int SubscriptionId { get; set; } + + /// RetryGroup.Id, which survives policy edits, so the total does too. + public Guid GroupId { get; set; } + + public int AttemptsUsed { get; set; } + + /// When the last attempt was claimed — the only clue left once a group is exhausted. + public DateTime LastAttemptOn { get; set; } + + /// + /// When the exhaustion alert for this integration and group was claimed, or null while + /// the budget still has room. + /// + /// + /// Claiming this is what makes the alert fire exactly once: every failure after the budget runs + /// out would otherwise raise another one. Reset deletes the whole row, which re-arms the alert + /// along with the budget. + /// + public DateTime? ExhaustedNotifiedOn { get; set; } +} diff --git a/SW.Bitween.Api/Domain/RetryPolicy.cs b/SW.Bitween.Api/Domain/RetryPolicy.cs index c1793896..c452e6f3 100644 --- a/SW.Bitween.Api/Domain/RetryPolicy.cs +++ b/SW.Bitween.Api/Domain/RetryPolicy.cs @@ -9,6 +9,15 @@ public class RetryPolicy : BaseEntity, IAudited, IRetryPolicy { public string Name { get; set; } public List Groups { get; set; } = []; + + /// + /// Default destination for "retry budget exhausted" alerts, used by every group that does not + /// override it. Null means no alert unless a group or a subscription+group override defines one. + /// + public string AlertHandlerId { get; set; } + + /// That adapter's own settings — api key, recipients, subject. + public IReadOnlyDictionary AlertHandlerProperties { get; set; } public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } public DateTime? ModifiedOn { get; set; } diff --git a/SW.Bitween.Api/Domain/Setting.cs b/SW.Bitween.Api/Domain/Setting.cs new file mode 100644 index 00000000..61d566e8 --- /dev/null +++ b/SW.Bitween.Api/Domain/Setting.cs @@ -0,0 +1,26 @@ +using System; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; + +/// +/// One instance-wide setting, keyed by the setting's catalog key (e.g. Theme.PrimaryColor). +/// This table is the single source of truth: configuration seeds a key once — on the first boot +/// after that key exists — and is ignored for it from then on. Every catalog key normally has a +/// row; "reset to default" rewrites the row with the product default rather than removing it. +/// +/// Deliberately a plain key/value store: the definition of a key (label, section, type, +/// whether it's a secret) lives in , so adding a +/// setting never needs a migration or a data fix-up. A secret's value is encrypted before it +/// gets here — see . +/// +/// +public class Setting : BaseEntity, IAudited +{ + public string Value { get; set; } + + public DateTime CreatedOn { get; set; } + public string CreatedBy { get; set; } + public DateTime? ModifiedOn { get; set; } + public string ModifiedBy { get; set; } +} diff --git a/SW.Bitween.Api/Domain/Subscription/Schedule.cs b/SW.Bitween.Api/Domain/Subscription/Schedule.cs index fe4909db..eaed7744 100644 --- a/SW.Bitween.Api/Domain/Subscription/Schedule.cs +++ b/SW.Bitween.Api/Domain/Subscription/Schedule.cs @@ -57,7 +57,11 @@ public override int GetHashCode() public DateTime Next(DateTime? currentDate = null) { - var utcNow = currentDate == null ? DateTime.UtcNow : currentDate.Value; + // On.Hours/On.Minutes are UTC wall-clock time — the Quartz cron trigger that + // actually fires this schedule is explicitly pinned to TimeZoneInfo.Utc + // (see SW.Scheduler's WithCronSchedule(...).InTimeZone(TimeZoneInfo.Utc) call + // sites), so this must agree rather than each side defaulting independently. + var utcNow = currentDate ?? DateTime.UtcNow; DateTime nextSelectedDate; switch (Recurrence) diff --git a/SW.Bitween.Api/Domain/Xchange/Xchange.cs b/SW.Bitween.Api/Domain/Xchange/Xchange.cs index e9389a36..164ae8f7 100644 --- a/SW.Bitween.Api/Domain/Xchange/Xchange.cs +++ b/SW.Bitween.Api/Domain/Xchange/Xchange.cs @@ -60,9 +60,10 @@ public Xchange(Subscription subscription, XchangeFile file, string[] references } //retry xchange - public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup, IReadOnlyDictionary groupAttemptCounts = null) : + public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup, bool manualRetry = false) : this(xchange.DocumentId, workGroup, file, xchange.References) { + ManualRetry = manualRetry; SubscriptionId = xchange.SubscriptionId; PartnerId = xchange.PartnerId; MapperId = xchange.MapperId; @@ -72,23 +73,24 @@ public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup, IReadOnl ResponseSubscriptionId = xchange.ResponseSubscriptionId; RetryFor = xchange.Id; CorrelationId = xchange.CorrelationId; - GroupAttemptCounts = groupAttemptCounts == null ? null : new Dictionary(groupAttemptCounts); } //retry with reset subscription properties - public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, IReadOnlyDictionary groupAttemptCounts = null) : + public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, Partner gatewayPartner = null, + GlobalAdapterValuesSet[] globalAdapterValuesSets = null, IReadOnlyDictionary groupAttemptCounts = null, + bool manualRetry = false) : this(xchange.DocumentId, subscription.WorkGroup, file, xchange.References) { + ManualRetry = manualRetry; SubscriptionId = xchange.SubscriptionId; PartnerId = xchange.PartnerId ?? subscription.PartnerId; MapperId = subscription.MapperId; HandlerId = subscription.HandlerId; - MapperProperties = subscription.MapperProperties; - HandlerProperties = subscription.HandlerProperties; + MapperProperties = (subscription.MapperProperties ?? new Dictionary()).ToDictionary().Fill(gatewayPartner, globalAdapterValuesSets); + HandlerProperties = (subscription.HandlerProperties ?? new Dictionary()).ToDictionary().Fill(gatewayPartner, globalAdapterValuesSets); ResponseSubscriptionId = subscription.ResponseSubscriptionId; RetryFor = xchange.Id; CorrelationId = xchange.CorrelationId; - GroupAttemptCounts = groupAttemptCounts == null ? null : new Dictionary(groupAttemptCounts); } public int? SubscriptionId { get; private set; } @@ -108,7 +110,14 @@ public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, IRe public string ResponseMessageTypeName { get; private set; } public string RetryFor { get; private set; } + + /// + /// true when a person asked for this retry, rather than the retry policy scheduling + /// it. The policy leaves these alone, so pressing Retry never spends the group's shared + /// budget and never quietly starts an automatic chain behind the person who pressed it. + /// + public bool ManualRetry { get; private set; } + public string CorrelationId { get; set; } - public IReadOnlyDictionary GroupAttemptCounts { get; private set; } } } \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/XchangeNotification.cs b/SW.Bitween.Api/Domain/XchangeNotification.cs index 56a52186..97e8094b 100644 --- a/SW.Bitween.Api/Domain/XchangeNotification.cs +++ b/SW.Bitween.Api/Domain/XchangeNotification.cs @@ -5,9 +5,12 @@ namespace SW.Bitween.Domain { public class XchangeNotification:BaseEntity { + /// Name recorded for rows written by the retry-budget alert rather than a notifier. + public const string RetryBudgetAlertName = "Retry budget alert"; + private XchangeNotification(){} - public XchangeNotification(string xchangeId, int notifierId, string notifierName, string exception = null) + public XchangeNotification(string xchangeId, int? notifierId, string notifierName, string exception = null) { XchangeId = xchangeId; FinishedOn = DateTime.UtcNow; @@ -16,12 +19,21 @@ public XchangeNotification(string xchangeId, int notifierId, string notifierName NotifierId = notifierId; NotifierName = notifierName; } - + + /// + /// Logs an attempt to deliver a "retry budget exhausted" alert. These rows have no + /// because the alert is configured on the retry policy rather than + /// on a notifier — which is also how the send is recognised as already done on a redelivery. + /// + public static XchangeNotification ForRetryBudgetAlert(string xchangeId, string exception = null) => + new(xchangeId, null, RetryBudgetAlertName, exception); + public string XchangeId { get; private set; } public bool Success { get; set; } - public int NotifierId { get; set; } + /// The notifier that produced this row, or null for a retry-budget alert. + public int? NotifierId { get; set; } public string NotifierName { get; set; } diff --git a/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs b/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs index ac674785..ed666dee 100644 --- a/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs +++ b/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs @@ -62,7 +62,53 @@ public XchangeResult(string xchangeId,WorkGroup workGroup, XchangeFile outputFil public bool ResponseBad { get; private set; } public string ResponseContentType { get; private set; } + /// + /// Why the retry policy declined to schedule another attempt for this failure, or + /// null when a retry was scheduled or no policy applied. Without it a group that + /// has exhausted its budget looks identical to one that never matched. + /// + public string RetryBlockedReason { get; private set; } + /// Records the policy's refusal so it can be shown alongside the failure. + public void SetRetryBlocked(string reason) => RetryBlockedReason = reason; + /// + /// The retry group that matched this failure, or null when no policy applied or none + /// matched. The evaluator works this out and would otherwise discard it, leaving no way to + /// ask which failures a group is responsible for. + /// + public Guid? RetryGroupId { get; private set; } + + /// + /// How many times this message had already been attempted when the policy evaluated it + /// (0 on the original run). Stored because deriving it means walking the whole + /// Xchange.RetryFor chain one query at a time. + /// + public int? AttemptNumber { get; private set; } + + /// Records which group owned this failure, and how far into its retries it was. + public void SetRetryEvaluation(Guid groupId, int attemptNumber) + { + RetryGroupId = groupId; + AttemptNumber = attemptNumber; + } + + /// + /// Announces that this failure was the one that emptied the group's shared budget. Only ever + /// called by the caller that won the claim, so the event is raised once per exhaustion. + /// + public void RaiseBudgetExhausted(int subscriptionId, Guid groupId, string groupName, + int maxAttemptsTotal) + { + Events.Add(new RetryBudgetExhaustedEvent + { + XchangeId = Id, + SubscriptionId = subscriptionId, + GroupId = groupId, + GroupName = groupName, + MaxAttemptsTotal = maxAttemptsTotal, + OccurredOn = DateTime.UtcNow + }); + } } } diff --git a/SW.Bitween.Api/Extensions/AccountExtensions.cs b/SW.Bitween.Api/Extensions/AccountExtensions.cs index a0c3a6f5..a647579e 100644 --- a/SW.Bitween.Api/Extensions/AccountExtensions.cs +++ b/SW.Bitween.Api/Extensions/AccountExtensions.cs @@ -78,9 +78,6 @@ private static ClaimsIdentity CreateClaimsIdentity(this Account account, LoginMe case LoginMethod.EmailAndPassword: claims.Add(new Claim(ClaimTypes.Name, account.Email)); break; - case LoginMethod.PhoneAndOtp: - claims.Add(new Claim(ClaimTypes.Name, account.Phone)); - break; case LoginMethod.ApiKey: claims.Add(new Claim(ClaimTypes.Name, account.Id.ToString())); break; @@ -91,7 +88,6 @@ private static ClaimsIdentity CreateClaimsIdentity(this Account account, LoginMe } if (account.Email != null) claims.Add(new Claim(ClaimTypes.Email, account.Email)); - if (account.Phone != null) claims.Add(new Claim(ClaimTypes.MobilePhone, account.Phone)); return new ClaimsIdentity(claims, "Bitween"); diff --git a/SW.Bitween.Api/Extensions/ClaimsPrincipalExtensions.cs b/SW.Bitween.Api/Extensions/ClaimsPrincipalExtensions.cs deleted file mode 100644 index ec5900b7..00000000 --- a/SW.Bitween.Api/Extensions/ClaimsPrincipalExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Security.Claims; -using SW.Bitween.Domain.Accounts; - -namespace SW.Bitween -{ - static class ClaimsPrincipalExtensions - { - public static AccountRole? GetRole(this ClaimsPrincipal claimsPrincipal) - { - var role = claimsPrincipal?.FindFirst("Role"); - if (role is null) - return null; - return Enum.Parse(role.Value); - } - } -} \ No newline at end of file diff --git a/SW.Bitween.Api/Extensions/PasswordValidationExtensions.cs b/SW.Bitween.Api/Extensions/PasswordValidationExtensions.cs new file mode 100644 index 00000000..3da0b26a --- /dev/null +++ b/SW.Bitween.Api/Extensions/PasswordValidationExtensions.cs @@ -0,0 +1,17 @@ +using FluentValidation; + +namespace SW.Bitween +{ + public static class PasswordValidationExtensions + { + // Shared server-side password policy so every password-setting path + // (create account, change password) enforces the exact same rule. + public static IRuleBuilderOptions Password(this IRuleBuilder rule) => + rule.NotEmpty().WithMessage("Password is required.") + .MinimumLength(8).WithMessage("Password must be at least 8 characters.") + .Matches("[A-Z]").WithMessage("Password must contain an uppercase letter.") + .Matches("[a-z]").WithMessage("Password must contain a lowercase letter.") + .Matches("[0-9]").WithMessage("Password must contain a number.") + .Matches("[^A-Za-z0-9]").WithMessage("Password must contain a special character."); + } +} diff --git a/SW.Bitween.Api/Extensions/RequestContextExtensions.cs b/SW.Bitween.Api/Extensions/RequestContextExtensions.cs index 32de6bfe..93f5bb8f 100644 --- a/SW.Bitween.Api/Extensions/RequestContextExtensions.cs +++ b/SW.Bitween.Api/Extensions/RequestContextExtensions.cs @@ -1,17 +1,77 @@ +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; using SW.PrimitiveTypes; namespace SW.Bitween { public static class RequestContextExtensions { - public static void EnsureAccess(this RequestContext requestContext, params AccountRole[] allowedRoles) + /// + /// Marks the break-glass token minted by POST /login from configured AdminCredentials. That + /// token has no account behind it, so its grants can't be resolved from the database. It + /// used to clear every check only because the old role guard failed open on a missing + /// claim; this claim makes the same grant deliberate instead of accidental. + /// + public const string SuperuserClaim = "bitween_superuser"; + + /// + /// Throws unless the caller holds at least one of . This is really a + /// "forbidden" — the caller is signed in and simply isn't allowed — but CqApi renders + /// SWForbiddenException as a 401 that's byte-identical to sending no token, so there is no + /// distinction to be had on the wire. The client answers a 401 by refreshing the token and + /// retrying once, which means every denial costs a wasted round trip. Worth revisiting if + /// CqApi ever maps forbidden to 403. + /// + public static async Task EnsurePermission(this RequestContext requestContext, BitweenDbContext dbContext, + params string[] anyOf) { + var granted = await requestContext.GetPermissions(dbContext); + if (!anyOf.Any(granted.Contains)) + throw new SWUnauthorizedException("INSUFFICIENT_PERMISSIONS"); + } - var jobRole = requestContext.User.GetRole(); - if (jobRole is not null && allowedRoles.All(a => a != jobRole)) + public static async Task HasPermission(this RequestContext requestContext, BitweenDbContext dbContext, + string permission) + { + var granted = await requestContext.GetPermissions(dbContext); + return granted.Contains(permission); + } + + /// + /// The union of every permission the caller's roles grant. Resolved from the database on + /// each call rather than carried in the token, so revoking a role takes effect immediately + /// instead of at token expiry. Handlers guard once, so that's one small indexed query. + /// + public static async Task> GetPermissions(this RequestContext requestContext, + BitweenDbContext dbContext) + { + if (requestContext.User?.FindFirst(SuperuserClaim) is not null) + return PermissionCatalog.AllKeys.ToHashSet(); + + // Fail closed: no identifiable account means no grants. + if (!int.TryParse(requestContext.GetNameIdentifier(), out var accountId)) throw new SWUnauthorizedException("INSUFFICIENT_PERMISSIONS"); + + return await GetPermissionsOf(dbContext, accountId); + } + + public static async Task> GetPermissionsOf(BitweenDbContext dbContext, int accountId) + { + var roles = await (from link in dbContext.Set() + join role in dbContext.Set() on link.RoleId equals role.Id + where link.AccountId == accountId + select new { role.Id, role.IsSystem, role.Permissions }) + .AsNoTracking() + .ToListAsync(); + + var granted = new HashSet(); + foreach (var role in roles) + granted.UnionWith(role.IsSystem ? Role.SystemPermissions(role.Id) : role.Permissions ?? []); + return granted; } } -} \ No newline at end of file +} diff --git a/SW.Bitween.Api/Resources/Accounts/AccountRoles.cs b/SW.Bitween.Api/Resources/Accounts/AccountRoles.cs new file mode 100644 index 00000000..6f6117e5 --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/AccountRoles.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +internal static class AccountRoles +{ + /// Role summaries for a set of accounts, in one query — keyed by account id. + public static async Task>> For(BitweenDbContext dbContext, + List accountIds) + { + var links = await (from link in dbContext.Set() + join role in dbContext.Set() on link.RoleId equals role.Id + where accountIds.Contains(link.AccountId) + select new { link.AccountId, role.Id, role.Name }) + .AsNoTracking() + .ToListAsync(); + + return links + .GroupBy(l => l.AccountId) + .ToDictionary( + g => g.Key, + g => g.OrderBy(l => l.Name) + .Select(l => new AccountRoleSummary { Id = l.Id, Name = l.Name }) + .ToList()); + } + + public static async Task> Of(BitweenDbContext dbContext, int accountId) => + (await For(dbContext, [accountId])).GetValueOrDefault(accountId, []); + + /// + /// Replaces an account's roles. Rejects unknown ids so a stale UI can't silently strip access. + /// + public static async Task Set(BitweenDbContext dbContext, int accountId, List roleIds) + { + var wanted = (roleIds ?? []).Distinct().ToList(); + + var known = await dbContext.Set().Where(r => wanted.Contains(r.Id)).Select(r => r.Id).ToListAsync(); + var unknown = wanted.Except(known).ToList(); + if (unknown.Count > 0) + throw new SWValidationException("ROLE_NOT_FOUND", + $"These roles don't exist: {string.Join(", ", unknown)}."); + + var existing = await dbContext.Set().Where(l => l.AccountId == accountId).ToListAsync(); + + foreach (var link in existing.Where(l => !wanted.Contains(l.RoleId))) + dbContext.Remove(link); + + foreach (var roleId in wanted.Except(existing.Select(l => l.RoleId))) + dbContext.Add(new AccountRoleLink(accountId, roleId)); + } + + /// + /// Keeps the legacy column meaningful for older API consumers. It no + /// longer drives authorization — it's derived from whichever built-in role the member holds, + /// falling back to Member for someone who only holds custom roles. + /// + public static AccountRole LegacyRoleFor(List roleIds) + { + if (roleIds.Contains(Role.AdministratorId)) return AccountRole.Admin; + if (roleIds.Contains(Role.MemberId)) return AccountRole.Member; + if (roleIds.Count == 0 || roleIds.Contains(Role.ViewerId)) return AccountRole.Viewer; + return AccountRole.Member; + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/Administrators.cs b/SW.Bitween.Api/Resources/Accounts/Administrators.cs new file mode 100644 index 00000000..fbd18109 --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/Administrators.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +internal static class Administrators +{ + /// + /// How many other enabled accounts still hold the Administrator role. Guards every operation + /// that could otherwise leave an instance with nobody able to manage members and roles. + /// + public static Task OtherThan(BitweenDbContext dbContext, int accountId) => + (from link in dbContext.Set() + join account in dbContext.Set() on link.AccountId equals account.Id + where link.RoleId == Role.AdministratorId && link.AccountId != accountId && !account.Disabled + select link.AccountId).CountAsync(); + + public static Task Holds(BitweenDbContext dbContext, int accountId) => + dbContext.Set() + .AnyAsync(l => l.AccountId == accountId && l.RoleId == Role.AdministratorId); + + /// + /// Blocks an operation that would remove the last administrator. Only applies when the account + /// actually holds the role — otherwise nothing is being taken away. + /// + public static async Task EnsureNotTheLast(BitweenDbContext dbContext, int accountId) + { + if (!await Holds(dbContext, accountId)) + return; + + if (await OtherThan(dbContext, accountId) == 0) + throw new SWValidationException("LAST_ADMINISTRATOR", + "This is the only member with the Administrator role. Give it to someone else first."); + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs b/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs index 40ade9aa..bd06695c 100644 --- a/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs +++ b/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using FluentValidation; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; @@ -21,8 +22,8 @@ public ChangePassword(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(ChangePasswordModel request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member, AccountRole.Viewer); - + // Self-service: this only ever changes the caller's own password, and the old one has to + // be supplied. The guard it replaces listed every role, so it granted nothing. var accountId = Convert.ToInt32(_requestContext.GetNameIdentifier()); var account = await _dbContext.Set().FindAsync(accountId); @@ -37,4 +38,12 @@ public async Task Handle(ChangePasswordModel request) return null; } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.NewPassword).Password(); + } + } } \ No newline at end of file diff --git a/SW.Bitween.Api/Resources/Accounts/Create.cs b/SW.Bitween.Api/Resources/Accounts/Create.cs index f8e10142..83dd0ec8 100644 --- a/SW.Bitween.Api/Resources/Accounts/Create.cs +++ b/SW.Bitween.Api/Resources/Accounts/Create.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using FluentValidation; using Microsoft.EntityFrameworkCore; @@ -23,7 +24,7 @@ public Create(BitweenDbContext dbContext, RequestContext requestContext, Bitween public async Task Handle(CreateAccountModel request) { - _requestContext.EnsureAccess(AccountRole.Admin); + await _requestContext.EnsurePermission(dbContext, Model.Permissions.Users.Create); if (string.IsNullOrEmpty(request.Name) || string.IsNullOrEmpty(request.Email) || (!_bitweenOptions.DisableEmailPasswordLogin && string.IsNullOrEmpty(request.Password))) @@ -32,6 +33,17 @@ public async Task Handle(CreateAccountModel request) if (await dbContext.Set().AnyAsync(a => a.Email == request.Email)) throw new SWValidationException("ACCOUNT_EXISTS", $"Account with email {request.Email} exists"); + // Prefer explicit role ids, and fall back to the legacy coarse role only when one was + // actually sent. It used to be a plain int, so omitting it meant 0 — which is Admin — + // and adding a member with no roles ticked handed them the run of the instance. + var roleIds = request.RoleIds is { Count: > 0 } + ? request.RoleIds.Distinct().ToList() + : request.Role is null + ? [] + : [BuiltInRoleFor((AccountRole)request.Role)]; + + // No password at all when this instance signs in through Microsoft only — the account + // exists purely to be matched by email. var password = _bitweenOptions.DisableEmailPasswordLogin ? null : SecurePasswordHasher.Hash(request.Password); @@ -40,21 +52,30 @@ public async Task Handle(CreateAccountModel request) request.Name, request.Email, password, - (AccountRole)request.Role); + AccountRoles.LegacyRoleFor(roleIds)); dbContext.Add(newAccount); + await dbContext.SaveChangesAsync(); + await AccountRoles.Set(dbContext, newAccount.Id, roleIds); await dbContext.SaveChangesAsync(); - return null; + return newAccount.Id; } + private static int BuiltInRoleFor(AccountRole role) => role switch + { + AccountRole.Admin => Role.AdministratorId, + AccountRole.Member => Role.MemberId, + _ => Role.ViewerId + }; + private class Validate : AbstractValidator { public Validate(BitweenOptions bitweenOptions) { RuleFor(i => i.Name).NotEmpty(); RuleFor(i => i.Email).NotEmpty(); - RuleFor(i => i.Password).NotEmpty().When(_ => !bitweenOptions.DisableEmailPasswordLogin); + RuleFor(i => i.Password).Password().When(_ => !bitweenOptions.DisableEmailPasswordLogin); RuleFor(i => i.Role).NotNull(); } } diff --git a/SW.Bitween.Api/Resources/Accounts/Login.cs b/SW.Bitween.Api/Resources/Accounts/Login.cs index b031b2bf..e47bc8dd 100644 --- a/SW.Bitween.Api/Resources/Accounts/Login.cs +++ b/SW.Bitween.Api/Resources/Accounts/Login.cs @@ -15,6 +15,9 @@ namespace SW.Bitween.Resources.Accounts [Unprotect] public class Login : ICommandHandler { + private const int MaxFailedLoginAttempts = 5; + private static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15); + private readonly BitweenDbContext _dbContext; private readonly BitweenOptions _BitweenSettings; private readonly JwtTokenParameters _jwtTokenParameters; @@ -68,6 +71,16 @@ public async Task Handle(UserLogin request) throw new SWException("Email and password login is disabled. Please sign in with Microsoft."); } + // A credential login must carry both a username and a password. Without this guard a + // request with a valid username but empty/missing password would skip verification + // below and still be issued a token. + if (string.IsNullOrEmpty(refreshTokenValue) && string.IsNullOrEmpty(request.MsToken) && + (string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))) + { + _logger.LogWarning("Login rejected: missing username or password on a credential login."); + throw new SWException("Invalid username or password."); + } + if (!string.IsNullOrEmpty(refreshTokenValue)) { // account query already filtered above @@ -118,20 +131,45 @@ public async Task Handle(UserLogin request) if (string.IsNullOrEmpty(refreshTokenValue) && !string.IsNullOrEmpty(request.Username) && !string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.MsToken)) { + var nowUtc = DateTime.UtcNow; + if (account.IsLockedOut(nowUtc)) + { + var minutes = (int)Math.Ceiling((account.LockoutEnd!.Value - nowUtc).TotalMinutes); + _logger.LogWarning("Login rejected: account '{Email}' is temporarily locked.", account.Email); + throw new SWException( + $"Your account is temporarily locked due to multiple failed login attempts. " + + $"Please try again in {minutes} minute{(minutes == 1 ? "" : "s")}."); + } + if (request.Password == null || !SecurePasswordHasher.Verify(request.Password, account.Password)) + { + // Atomic DB-side update so concurrent wrong-password attempts can't read the + // same count and lose increments, which would let them slip past the lockout. + var lockoutEnd = nowUtc.Add(LockoutDuration); + await _dbContext.Set() + .Where(a => a.Id == account.Id) + .ExecuteUpdateAsync(s => s + .SetProperty(a => a.LockoutEnd, + a => a.FailedLoginCount + 1 >= MaxFailedLoginAttempts ? lockoutEnd : a.LockoutEnd) + .SetProperty(a => a.FailedLoginCount, + a => a.FailedLoginCount + 1 >= MaxFailedLoginAttempts ? 0 : a.FailedLoginCount + 1)); throw new SWException("Invalid username or password."); + } + + account.RegisterSuccessfulLogin(); } var newRefreshToken = CreateRefreshToken(account, LoginMethod.EmailAndPassword); await _dbContext.SaveChangesAsync(); - // Set refresh token as HttpOnly cookie — not accessible to JavaScript - var isHttps = _httpContextAccessor.HttpContext?.Request.IsHttps ?? false; + // Set refresh token as a secure, HttpOnly cookie — not accessible to JavaScript. + // Secure is always on: the app is served over HTTPS, and TLS is terminated at the + // reverse proxy, so Request.IsHttps would otherwise be false and drop the attribute. _httpContextAccessor.HttpContext?.Response.Cookies.Append("refresh_token", newRefreshToken, new CookieOptions { HttpOnly = true, - Secure = isHttps, + Secure = true, SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddDays(30) }); diff --git a/SW.Bitween.Api/Resources/Accounts/Logout.cs b/SW.Bitween.Api/Resources/Accounts/Logout.cs index 5a8826ac..e6fe503e 100644 --- a/SW.Bitween.Api/Resources/Accounts/Logout.cs +++ b/SW.Bitween.Api/Resources/Accounts/Logout.cs @@ -40,6 +40,9 @@ public async Task Handle(UserLogout request) httpContext.Response.Cookies.Delete("refresh_token"); } + // Tell the browser to wipe cookies, cache and storage for this origin on logout. + httpContext?.Response.Headers.Append("Clear-Site-Data", "\"cache\", \"cookies\", \"storage\""); + return new { }; } } diff --git a/SW.Bitween.Api/Resources/Accounts/Profile.cs b/SW.Bitween.Api/Resources/Accounts/Profile.cs index 18eced18..ce4cf093 100644 --- a/SW.Bitween.Api/Resources/Accounts/Profile.cs +++ b/SW.Bitween.Api/Resources/Accounts/Profile.cs @@ -8,6 +8,10 @@ namespace SW.Bitween.Resources.Accounts; +/// +/// Who am I and what may I do. Deliberately ungated — every signed-in caller needs it, and the +/// permissions it returns are what the UI uses to decide which pages and actions to show. +/// [HandlerName("profile")] public class Profile : IQueryHandler { @@ -23,16 +27,26 @@ public Profile(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle() { var accountId = Convert.ToInt32(requestContext.GetNameIdentifier()); - return await dbContext.Set() + + var profile = await dbContext.Set() .AsNoTracking() - .Select(a => new AccountModel + .Where(a => a.Id == accountId) + .Select(a => new ProfileModel { CreatedOn = a.CreatedOn, Email = a.Email, Name = a.DisplayName, Id = a.Id, + Disabled = a.Disabled, Role = a.Role.ToString() }) - .FirstOrDefaultAsync(a => a.Id == accountId); + .SingleOrDefaultAsync(); + + if (profile is null) + return null; + + profile.Roles = await AccountRoles.Of(dbContext, accountId); + profile.Permissions = (await requestContext.GetPermissions(dbContext)).OrderBy(p => p).ToList(); + return profile; } -} \ No newline at end of file +} diff --git a/SW.Bitween.Api/Resources/Accounts/RemoveAccount.cs b/SW.Bitween.Api/Resources/Accounts/RemoveAccount.cs index a93a6b50..dabe8792 100644 --- a/SW.Bitween.Api/Resources/Accounts/RemoveAccount.cs +++ b/SW.Bitween.Api/Resources/Accounts/RemoveAccount.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using SW.Bitween.Domain.Accounts; using SW.PrimitiveTypes; @@ -18,13 +19,18 @@ public RemoveAccountModel(BitweenDbContext dbContext, RequestContext requestCont public async Task Handle(int key, RemoveAccountModel request) { - _requestContext.EnsureAccess(AccountRole.Admin); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Users.Delete); var account = await _dbContext.Set().FindAsync(key); if (account is null) throw new SWValidationException("ACCOUNT_NOT_FOUND", $"Account with {key} was not found"); + if (key == Convert.ToInt32(_requestContext.GetNameIdentifier())) + throw new SWValidationException("CANNOT_REMOVE_SELF", "You can't remove your own account."); + + await Administrators.EnsureNotTheLast(_dbContext, key); + _dbContext.Remove(account); await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/Accounts/Search.cs b/SW.Bitween.Api/Resources/Accounts/Search.cs index fd7afebc..3c32e5ed 100644 --- a/SW.Bitween.Api/Resources/Accounts/Search.cs +++ b/SW.Bitween.Api/Resources/Accounts/Search.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -7,24 +8,31 @@ namespace SW.Bitween.Resources.Accounts { - public class Search : IQueryHandler + public class Search : IQueryHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } public async Task Handle(SearchMembersModel request) { + // Lookup returns only id/name pairs, which document and integration trails use to turn + // createdBy ids into names; the member list itself is the data users.view covers. + if (!request.Lookup) + await requestContext.EnsurePermission(dbContext, Model.Permissions.Users.View); + request.Limit ??= 20; request.Offset ??= 0; var query = dbContext.Set().AsNoTracking().AsQueryable(); - if (request.Lookup) { + // id -> display name only; needed across the app (e.g. audit trails) return await query.OrderBy(i => i.DisplayName) .ToDictionaryAsync(i => i.Id, i => i.DisplayName); } @@ -40,10 +48,17 @@ public async Task Handle(SearchMembersModel request) Email = a.Email, Name = a.DisplayName, Id = a.Id, - Role = a.Role.ToString() + Disabled = a.Disabled, + Role = a.Role.ToString(), + LockoutEnd = a.LockoutEnd }) .ToListAsync(); + var accountIds = accounts.Select(a => a.Id).ToList(); + var rolesByAccount = await AccountRoles.For(dbContext, accountIds); + + foreach (var account in accounts) + account.Roles = rolesByAccount.GetValueOrDefault(account.Id, []); return new { @@ -52,4 +67,4 @@ public async Task Handle(SearchMembersModel request) }; } } -} \ No newline at end of file +} diff --git a/SW.Bitween.Api/Resources/Accounts/SetDisabled.cs b/SW.Bitween.Api/Resources/Accounts/SetDisabled.cs new file mode 100644 index 00000000..94185d31 --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/SetDisabled.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +/// +/// Suspends or restores an account. A disabled account keeps its roles and history but can't sign +/// in — see the Disabled check in the login handler. +/// +[HandlerName("setDisabled")] +public class SetDisabled : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public SetDisabled(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, SetAccountDisabledModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Users.Edit); + + var account = await _dbContext.Set().FindAsync(key); + if (account is null) + throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); + + if (request.Disabled) + { + if (key == Convert.ToInt32(_requestContext.GetNameIdentifier())) + throw new SWValidationException("CANNOT_DISABLE_SELF", "You can't disable your own account."); + + await Administrators.EnsureNotTheLast(_dbContext, key); + } + + account.SetDisabled(request.Disabled); + await _dbContext.SaveChangesAsync(); + + return null; + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/SetPassword.cs b/SW.Bitween.Api/Resources/Accounts/SetPassword.cs new file mode 100644 index 00000000..65ef407e --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/SetPassword.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using FluentValidation; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +/// +/// Sets a member's password on their behalf. Bitween has no outbound mail, so there's no +/// self-service reset — without this, anyone who forgets their password is locked out for good. +/// Changing your own password goes through ChangePassword, which asks for the current one. +/// +[HandlerName("setPassword")] +public class SetPassword : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public SetPassword(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, SetAccountPasswordModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Users.Edit); + + if (key == Convert.ToInt32(_requestContext.GetNameIdentifier())) + throw new SWValidationException("USE_CHANGE_PASSWORD", + "Use Change password to set your own, so the current one is still required."); + + var account = await _dbContext.Set().FindAsync(key); + if (account is null) + throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); + + account.SetPassword(request.Password); + await _dbContext.SaveChangesAsync(); + + return null; + } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.Password).NotEmpty().MinimumLength(8); + } + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/SetRoles.cs b/SW.Bitween.Api/Resources/Accounts/SetRoles.cs new file mode 100644 index 00000000..779b165b --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/SetRoles.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +/// Replaces the whole set of roles a member holds. +[HandlerName("setRoles")] +public class SetRoles : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public SetRoles(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, SetAccountRolesModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Users.Edit); + + var account = await _dbContext.Set().FindAsync(key); + if (account is null) + throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); + + var roleIds = (request.RoleIds ?? []).Distinct().ToList(); + + // Don't let the last administrator be demoted — including by themselves. Otherwise an + // instance ends up with nobody able to manage members or roles. + if (!roleIds.Contains(Role.AdministratorId)) + await Administrators.EnsureNotTheLast(_dbContext, key); + + await AccountRoles.Set(_dbContext, key, roleIds); + account.SetRole(AccountRoles.LegacyRoleFor(roleIds)); + await _dbContext.SaveChangesAsync(); + + return null; + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/Unlock.cs b/SW.Bitween.Api/Resources/Accounts/Unlock.cs new file mode 100644 index 00000000..50c765cb --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/Unlock.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +[HandlerName("unlock")] +public class Unlock : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Unlock(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, UnlockAccountModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Users.Edit); + + var account = await _dbContext.Set().FindAsync(key); + if (account is null) + throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); + + account.Unlock(); + await _dbContext.SaveChangesAsync(); + + return null; + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/Update.cs b/SW.Bitween.Api/Resources/Accounts/Update.cs index d7b3a3e6..1a27b8a9 100644 --- a/SW.Bitween.Api/Resources/Accounts/Update.cs +++ b/SW.Bitween.Api/Resources/Accounts/Update.cs @@ -6,7 +6,7 @@ namespace SW.Bitween.Resources.Accounts; -public class Update : ICommandHandler +public class Update : ICommandHandler { private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; @@ -21,19 +21,35 @@ public async Task Handle(int key, UpdateAccountModel request) { var loggedInUserId = Convert.ToInt32(_requestContext.GetNameIdentifier()); + // Anyone may edit their own name; editing someone else needs the grant. if (key != loggedInUserId) - _requestContext.EnsureAccess(AccountRole.Admin); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Users.Edit); - - var account = await _dbContext.Set().FindAsync(key); if (account is null) throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); + account.UpdateProfile(request.Name); + + // Changing a role is never self-service, or anyone could promote themselves. + if (request.Role is not null && (AccountRole)request.Role != account.Role) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Users.Edit); + var role = (AccountRole)request.Role; + account.SetRole(role); + await AccountRoles.Set(_dbContext, key, [BuiltInRoleFor(role)]); + } - account.Update(request.Name, (AccountRole)request.Role); await _dbContext.SaveChangesAsync(); return null; } -} \ No newline at end of file + + /// Maps the legacy coarse role onto the built-in role that reproduces it. + private static int BuiltInRoleFor(AccountRole role) => role switch + { + AccountRole.Admin => Role.AdministratorId, + AccountRole.Member => Role.MemberId, + _ => Role.ViewerId + }; +} diff --git a/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs b/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs index 78e08409..55edeed6 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs @@ -2,10 +2,10 @@ using SW.Bitween.Model; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; using Microsoft.EntityFrameworkCore; using System.Linq; using SW.Bitween.Domain; +using SW.Bitween.Resources.Subscriptions; namespace SW.Bitween.Resources.ApiGateways { @@ -14,16 +14,19 @@ public class AddPartner : ICommandHandler { private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; + private readonly AdapterRequirements _adapterRequirements; - public AddPartner(BitweenDbContext dbContext, RequestContext requestContext) + public AddPartner(BitweenDbContext dbContext, RequestContext requestContext, + AdapterRequirements adapterRequirements) { _dbContext = dbContext; _requestContext = requestContext; + _adapterRequirements = adapterRequirements; } public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.Edit); var gateway = await _dbContext.Set() .Include(ag => ag.Partners) @@ -32,31 +35,50 @@ public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) if (gateway == null) throw new SWNotFoundException($"ApiGateway with Id {gatewayId} not found"); - // Validate subscription exists and is of type GatewayApiCall - var subscription = await _dbContext.Set() - .FirstOrDefaultAsync(s => s.Id == model.SubscriptionId); - - if (subscription == null) - throw new SWNotFoundException($"Subscription with Id {model.SubscriptionId} not found"); - - if (subscription.Type != SubscriptionType.GatewayApiCall) - throw new SWException($"Subscription must be of type GatewayApiCall. Current type: {subscription.Type}"); - - // Check if partner already exists - var existingPartner = gateway.Partners != null - ? gateway.Partners.FirstOrDefault(p => p.PartnerId == model.PartnerId && p.SubscriptionId == model.SubscriptionId) - : null; - - if (existingPartner != null) - throw new SWException("Partner already exists in this gateway"); + InlineIntegration.EnsureExactlyOne(model.SubscriptionId, model.NewIntegration); var partnerLink = new ApiGatewayPartner { ApiGatewayId = gatewayId, - PartnerId = model.PartnerId, - SubscriptionId = model.SubscriptionId + PartnerId = model.PartnerId }; + if (model.NewIntegration != null) + { + // Staged, not saved — the attachment takes its foreign key from the subscription EF + // is tracking, so the pair lands on the one SaveChangesAsync below or not at all. + // Nothing to check for a duplicate against: an integration that does not exist yet + // cannot already be attached. + // An API gateway is not bound to an information type the way a bus gateway is, + // so this one comes from the caller. + var integration = await InlineIntegration.Stage( + _dbContext, _adapterRequirements, model.NewIntegration, + model.NewIntegration.DocumentId, SubscriptionType.GatewayApiCall); + partnerLink.Subscription = integration; + } + else + { + // Validate subscription exists and is of type GatewayApiCall + var subscription = await _dbContext.Set() + .FirstOrDefaultAsync(s => s.Id == model.SubscriptionId.Value); + + if (subscription == null) + throw new SWNotFoundException($"Subscription with Id {model.SubscriptionId} not found"); + + if (subscription.Type != SubscriptionType.GatewayApiCall) + throw new SWException($"Subscription must be of type GatewayApiCall. Current type: {subscription.Type}"); + + // Check if partner already exists + var existingPartner = gateway.Partners != null + ? gateway.Partners.FirstOrDefault(p => p.PartnerId == model.PartnerId && p.SubscriptionId == model.SubscriptionId) + : null; + + if (existingPartner != null) + throw new SWException("Partner already exists in this gateway"); + + partnerLink.SubscriptionId = model.SubscriptionId.Value; + } + _dbContext.Add(partnerLink); await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/ApiGateways/Create.cs b/SW.Bitween.Api/Resources/ApiGateways/Create.cs index c4702745..4462994c 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Create.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Create.cs @@ -2,7 +2,6 @@ using SW.Bitween.Model; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.ApiGateways { @@ -19,15 +18,15 @@ public Create(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(ApiGatewayCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.Create); - if (string.IsNullOrWhiteSpace(model.UrlName)) - throw new SWException("UrlName is required"); + GatewayUrlName.Validate(model.UrlName); var entity = new ApiGateway { Name = model.Name, - UrlName = model.UrlName + UrlName = model.UrlName, + Inactive = model.Inactive }; _dbContext.Add(entity); diff --git a/SW.Bitween.Api/Resources/ApiGateways/Delete.cs b/SW.Bitween.Api/Resources/ApiGateways/Delete.cs index 88143b5a..bea7de8a 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Delete.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Delete.cs @@ -1,8 +1,8 @@ +using Microsoft.EntityFrameworkCore; using SW.EfCoreExtensions; using SW.Bitween.Domain.Gateway; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.ApiGateways { @@ -19,9 +19,21 @@ public Delete(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.Delete); - await _dbContext.DeleteByKeyAsync(key); + var gateway = await _dbContext.Set() + .Include(ag => ag.Partners) + .FirstOrDefaultAsync(ag => ag.Id == key); + + if (gateway == null) + throw new SWNotFoundException($"ApiGateway with Id {key} not found"); + + // Partners are FK-restricted to the gateway; remove them explicitly before the gateway. + if (gateway.Partners != null && gateway.Partners.Count > 0) + _dbContext.RemoveRange(gateway.Partners); + + _dbContext.Remove(gateway); + await _dbContext.SaveChangesAsync(); return null; } } diff --git a/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs b/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs new file mode 100644 index 00000000..3742d3e7 --- /dev/null +++ b/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs @@ -0,0 +1,27 @@ +using System.Text.RegularExpressions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ApiGateways; + +/// +/// The url name is a path segment — partners call /api/Gateway/{urlName}/sync — so +/// anything needing escaping there makes a gateway that reads as configured and cannot be +/// reached. A space is the one that actually happens: it saves, the endpoint shown on the +/// page is the one the partner copies, and the call 404s with nothing on screen to explain it. +/// +internal static partial class GatewayUrlName +{ + [GeneratedRegex("^[a-z0-9]+(?:[-_][a-z0-9]+)*$")] + private static partial Regex Allowed(); + + public static void Validate(string urlName) + { + if (string.IsNullOrWhiteSpace(urlName)) + throw new SWException("UrlName is required"); + + if (!Allowed().IsMatch(urlName)) + throw new SWValidationException("GATEWAY_URL_NAME_INVALID", + $"'{urlName}' cannot be used in a URL. Use lowercase letters, digits, hyphens " + + "and underscores only — no spaces, and not starting or ending with a separator."); + } +} diff --git a/SW.Bitween.Api/Resources/ApiGateways/Get.cs b/SW.Bitween.Api/Resources/ApiGateways/Get.cs index 0b935c0b..23397cea 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Get.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Get.cs @@ -10,14 +10,18 @@ namespace SW.Bitween.Resources.ApiGateways public class Get : IGetHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; - public Get(BitweenDbContext dbContext) + public Get(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; } public async Task Handle(int key) { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.View); + var gateway = await _dbContext.Set() .AsNoTracking() .Include(ag => ag.Partners) @@ -34,6 +38,7 @@ public async Task Handle(int key) Id = gateway.Id, Name = gateway.Name, UrlName = gateway.UrlName, + Inactive = gateway.Inactive, PartnersCount = gateway.Partners.Count, Partners = gateway.Partners.Select(p => new ApiGatewayPartnerDto { diff --git a/SW.Bitween.Api/Resources/ApiGateways/RemovePartner.cs b/SW.Bitween.Api/Resources/ApiGateways/RemovePartner.cs index 610ac43d..1dbca5e8 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/RemovePartner.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/RemovePartner.cs @@ -1,7 +1,6 @@ using SW.Bitween.Domain.Gateway; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; using Microsoft.EntityFrameworkCore; using System.Linq; @@ -21,7 +20,7 @@ public RemovePartner(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int gatewayId, RemovePartnerRequest request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.Edit); var gateway = await _dbContext.Set() .Include(ag => ag.Partners) diff --git a/SW.Bitween.Api/Resources/ApiGateways/Search.cs b/SW.Bitween.Api/Resources/ApiGateways/Search.cs index b22f722f..ca00a616 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Search.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Search.cs @@ -11,20 +11,28 @@ namespace SW.Bitween.Resources.ApiGateways public class Search : ISearchyHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; } public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.View); + var query = from gateway in _dbContext.Set() select new ApiGatewayRow { Id = gateway.Id, Name = gateway.Name, UrlName = gateway.UrlName, + Inactive = gateway.Inactive, PartnersCount = gateway.Partners.Count }; @@ -38,10 +46,35 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa // Apply ordering by Id descending query = query.OrderByDescending(g => g.Id); + var totalCount = await query.Search(searchyRequest.Conditions).CountAsync(); + var result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync(); + + // The list screen needs full attachment detail up front (not just a + // count), so hydrate it with one grouped query instead of Get.cs's + // per-row Include (gateways are few, so this stays a single round trip). + var ids = result.Select(r => r.Id).ToList(); + var partnersByGateway = (await _dbContext.Set() + .AsNoTracking() + .Where(p => ids.Contains(p.ApiGatewayId)) + .Include(p => p.Partner) + .Include(p => p.Subscription) + .ToListAsync()) + .ToLookup(p => p.ApiGatewayId); + foreach (var row in result) + { + row.Partners = partnersByGateway[row.Id].Select(p => new ApiGatewayPartnerDto + { + PartnerId = p.PartnerId, + SubscriptionId = p.SubscriptionId, + PartnerName = p.Partner.Name, + SubscriptionName = p.Subscription.Name + }).ToList(); + } + return new SearchyResponse { - TotalCount = await query.Search(searchyRequest.Conditions).CountAsync(), - Result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync() + TotalCount = totalCount, + Result = result }; } } diff --git a/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs b/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs new file mode 100644 index 00000000..5590e6b3 --- /dev/null +++ b/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Gateway; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using System.Linq; +using System.Threading.Tasks; + +namespace SW.Bitween.Resources.ApiGateways +{ + /// + /// Paged, searched view of one gateway's attachments — the full list lives on + /// for callers that need every attached partner id (e.g. the + /// attach-partner picker's exclude list), this is only for the gateway page's own table. + /// + [HandlerName("attachments")] + public class SearchAttachments : IQueryHandler + { + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public SearchAttachments(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(SearchApiGatewayAttachmentsModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.View); + + var offset = request.Offset ?? 0; + var limit = request.Limit ?? 25; + var term = request.Search?.Trim(); + + var query = _dbContext.Set() + .AsNoTracking() + .Where(p => p.ApiGatewayId == request.ApiGatewayId); + + if (!string.IsNullOrEmpty(term)) + query = query.Where(p => p.Partner.Name.Contains(term) || p.Subscription.Name.Contains(term)); + + var totalCount = await query.CountAsync(); + + var result = await query + .OrderBy(p => p.Partner.Name) + .Skip(offset) + .Take(limit) + .Select(p => new ApiGatewayPartnerDto + { + PartnerId = p.PartnerId, + SubscriptionId = p.SubscriptionId, + PartnerName = p.Partner.Name, + SubscriptionName = p.Subscription.Name + }) + .ToListAsync(); + + return new + { + Result = result, + TotalCount = totalCount + }; + } + } +} diff --git a/SW.Bitween.Api/Resources/ApiGateways/Update.cs b/SW.Bitween.Api/Resources/ApiGateways/Update.cs index b53bef93..489cc936 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Update.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Update.cs @@ -3,7 +3,6 @@ using SW.Bitween.Model; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; using Microsoft.EntityFrameworkCore; using System.Linq; @@ -22,7 +21,7 @@ public Update(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key, ApiGatewayUpdate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.Edit); var entity = await _dbContext.Set() .Include(ag => ag.Partners) @@ -31,11 +30,11 @@ public async Task Handle(int key, ApiGatewayUpdate model) if (entity == null) throw new SWNotFoundException($"ApiGateway with Id {key} not found"); - if (string.IsNullOrWhiteSpace(model.UrlName)) - throw new SWException("UrlName is required"); + GatewayUrlName.Validate(model.UrlName); entity.Name = model.Name; entity.UrlName = model.UrlName; + entity.Inactive = model.Inactive; await _dbContext.SaveChangesAsync(); return null; diff --git a/SW.Bitween.Api/Resources/ApiGateways/UpdatePartner.cs b/SW.Bitween.Api/Resources/ApiGateways/UpdatePartner.cs index 87405abb..03f543b0 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/UpdatePartner.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/UpdatePartner.cs @@ -2,7 +2,6 @@ using SW.Bitween.Model; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; using Microsoft.EntityFrameworkCore; using System.Linq; using SW.Bitween.Domain; @@ -23,7 +22,7 @@ public UpdatePartner(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.Edit); var gateway = await _dbContext.Set() .Include(ag => ag.Partners) @@ -33,6 +32,12 @@ public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) throw new SWNotFoundException($"ApiGateway with Id {gatewayId} not found"); // Validate subscription exists and is of type GatewayApiCall + // Repointing an existing attachment always names an integration that already + // exists; defining one inline is only for the attachment being created. + if (!model.SubscriptionId.HasValue) + throw new SWValidationException(GatewayLinkTarget.NeitherGiven, + "Pick the integration this partner runs."); + var subscription = await _dbContext.Set() .FirstOrDefaultAsync(s => s.Id == model.SubscriptionId); @@ -48,7 +53,7 @@ public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) if (partnerLink == null) throw new SWNotFoundException($"Partner with Id {model.PartnerId} not found in gateway {gatewayId}"); - partnerLink.SubscriptionId = model.SubscriptionId; + partnerLink.SubscriptionId = model.SubscriptionId.Value; await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/BusGateways/AddRoute.cs b/SW.Bitween.Api/Resources/BusGateways/AddRoute.cs index ef53e451..3d713c97 100644 --- a/SW.Bitween.Api/Resources/BusGateways/AddRoute.cs +++ b/SW.Bitween.Api/Resources/BusGateways/AddRoute.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Domain.Gateway; using SW.Bitween.Model; +using SW.Bitween.Resources.Subscriptions; using SW.PrimitiveTypes; using System.Threading.Tasks; @@ -15,16 +15,20 @@ public class AddRoute : ICommandHandler private readonly RequestContext _requestContext; private readonly IInfolinkCache _cache; - public AddRoute(BitweenDbContext dbContext, RequestContext requestContext, IInfolinkCache cache) + private readonly AdapterRequirements _adapterRequirements; + + public AddRoute(BitweenDbContext dbContext, RequestContext requestContext, IInfolinkCache cache, + AdapterRequirements adapterRequirements) { _dbContext = dbContext; _requestContext = requestContext; _cache = cache; + _adapterRequirements = adapterRequirements; } public async Task Handle(int gatewayId, BusGatewayRouteCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.BusGateways.Edit); var gateway = await _dbContext.Set() .FirstOrDefaultAsync(bg => bg.Id == gatewayId); @@ -32,17 +36,32 @@ public async Task Handle(int gatewayId, BusGatewayRouteCreate model) if (gateway == null) throw new SWNotFoundException($"BusGateway with Id {gatewayId} not found"); - await ValidateSubscription(_dbContext, model.SubscriptionId, gateway.DocumentId); + InlineIntegration.EnsureExactlyOne(model.SubscriptionId, model.NewIntegration); await ValidatePartner(_dbContext, model.PartnerId); var route = new BusGatewayRoute { BusGatewayId = gatewayId, - SubscriptionId = model.SubscriptionId, PartnerId = model.PartnerId, MatchExpression = model.MatchExpression }; + if (model.NewIntegration != null) + { + // Staged, not saved: EF fills the route's foreign key from the subscription it is + // tracking, so both rows go in on the one SaveChangesAsync below. A route pointing + // at an integration that was never committed is not a state that can happen. + var integration = await InlineIntegration.Stage( + _dbContext, _adapterRequirements, model.NewIntegration, gateway.DocumentId, + SubscriptionType.BusGateway); + route.Subscription = integration; + } + else + { + await ValidateSubscription(_dbContext, model.SubscriptionId.Value, gateway.DocumentId); + route.SubscriptionId = model.SubscriptionId.Value; + } + _dbContext.Add(route); await _dbContext.SaveChangesAsync(); await _cache.BroadcastRevoke(); diff --git a/SW.Bitween.Api/Resources/BusGateways/Create.cs b/SW.Bitween.Api/Resources/BusGateways/Create.cs index bc7cd75e..fb85fa61 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Create.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Create.cs @@ -1,7 +1,6 @@ using FluentValidation; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Domain.Gateway; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -24,7 +23,7 @@ public Create(BitweenDbContext dbContext, RequestContext requestContext, IInfoli public async Task Handle(BusGatewayCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.BusGateways.Create); var documentExists = await _dbContext.Set().AnyAsync(d => d.Id == model.DocumentId); if (!documentExists) @@ -33,7 +32,8 @@ public async Task Handle(BusGatewayCreate model) var entity = new BusGateway { Name = model.Name, - DocumentId = model.DocumentId + DocumentId = model.DocumentId, + Inactive = model.Inactive }; _dbContext.Add(entity); diff --git a/SW.Bitween.Api/Resources/BusGateways/Delete.cs b/SW.Bitween.Api/Resources/BusGateways/Delete.cs index 0fbacf90..9958ec0d 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Delete.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Delete.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Domain.Gateway; using SW.PrimitiveTypes; using System.Threading.Tasks; @@ -21,7 +20,7 @@ public Delete(BitweenDbContext dbContext, RequestContext requestContext, IInfoli public async Task Handle(int key) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.BusGateways.Delete); var gateway = await _dbContext.Set() .Include(bg => bg.Routes) diff --git a/SW.Bitween.Api/Resources/BusGateways/Get.cs b/SW.Bitween.Api/Resources/BusGateways/Get.cs index 2eefa3db..ccaf6aa8 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Get.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Get.cs @@ -11,14 +11,18 @@ namespace SW.Bitween.Resources.BusGateways public class Get : IGetHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; - public Get(BitweenDbContext dbContext) + public Get(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; } public async Task Handle(int key) { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.BusGateways.View); + var gateway = await _dbContext.Set() .AsNoTracking() .Include(bg => bg.Routes) @@ -40,6 +44,7 @@ public async Task Handle(int key) Id = gateway.Id, Name = gateway.Name, DocumentId = gateway.DocumentId, + Inactive = gateway.Inactive, DocumentName = documentName, RoutesCount = gateway.Routes.Count, Routes = gateway.Routes.Select(r => new BusGatewayRouteDto diff --git a/SW.Bitween.Api/Resources/BusGateways/RemoveRoute.cs b/SW.Bitween.Api/Resources/BusGateways/RemoveRoute.cs index e4ea6e6f..4f573df1 100644 --- a/SW.Bitween.Api/Resources/BusGateways/RemoveRoute.cs +++ b/SW.Bitween.Api/Resources/BusGateways/RemoveRoute.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Domain.Gateway; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -23,7 +22,7 @@ public RemoveRoute(BitweenDbContext dbContext, RequestContext requestContext, II public async Task Handle(int gatewayId, RemoveRouteRequest request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.BusGateways.Edit); var route = await _dbContext.Set() .FirstOrDefaultAsync(r => r.Id == request.RouteId && r.BusGatewayId == gatewayId); diff --git a/SW.Bitween.Api/Resources/BusGateways/Search.cs b/SW.Bitween.Api/Resources/BusGateways/Search.cs index 3210d3cd..d59daa5b 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Search.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Search.cs @@ -12,14 +12,21 @@ namespace SW.Bitween.Resources.BusGateways public class Search : ISearchyHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; } public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.BusGateways.View); + var documents = _dbContext.Set(); var query = from gateway in _dbContext.Set() @@ -28,6 +35,7 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa Id = gateway.Id, Name = gateway.Name, DocumentId = gateway.DocumentId, + Inactive = gateway.Inactive, DocumentName = documents.Where(d => d.Id == gateway.DocumentId) .Select(d => d.Name).FirstOrDefault(), RoutesCount = gateway.Routes.Count @@ -42,10 +50,37 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa query = query.OrderByDescending(g => g.Id); + var totalCount = await query.Search(searchyRequest.Conditions).CountAsync(); + var result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync(); + + // The list screen needs full route detail up front (not just a count), + // so hydrate it with one grouped query instead of Get.cs's per-row + // Include (gateways are few, so this stays a single round trip). + var ids = result.Select(r => r.Id).ToList(); + var routesByGateway = (await _dbContext.Set() + .AsNoTracking() + .Where(r => ids.Contains(r.BusGatewayId)) + .Include(r => r.Subscription) + .Include(r => r.Partner) + .ToListAsync()) + .ToLookup(r => r.BusGatewayId); + foreach (var row in result) + { + row.Routes = routesByGateway[row.Id].Select(r => new BusGatewayRouteDto + { + Id = r.Id, + SubscriptionId = r.SubscriptionId, + SubscriptionName = r.Subscription != null ? r.Subscription.Name : null, + PartnerId = r.PartnerId, + PartnerName = r.Partner != null ? r.Partner.Name : null, + MatchExpression = r.MatchExpression + }).ToList(); + } + return new SearchyResponse { - TotalCount = await query.Search(searchyRequest.Conditions).CountAsync(), - Result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync() + TotalCount = totalCount, + Result = result }; } } diff --git a/SW.Bitween.Api/Resources/BusGateways/Update.cs b/SW.Bitween.Api/Resources/BusGateways/Update.cs index 4aa50cbd..51b0dcaa 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Update.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Update.cs @@ -1,6 +1,5 @@ using FluentValidation; using Microsoft.EntityFrameworkCore; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Domain.Gateway; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -23,7 +22,7 @@ public Update(BitweenDbContext dbContext, RequestContext requestContext, IInfoli public async Task Handle(int key, BusGatewayUpdate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.BusGateways.Edit); var entity = await _dbContext.Set() .FirstOrDefaultAsync(bg => bg.Id == key); @@ -33,6 +32,7 @@ public async Task Handle(int key, BusGatewayUpdate model) // Name only; the bound document is fixed at creation (routes' subscriptions belong to it). entity.Name = model.Name; + entity.Inactive = model.Inactive; await _dbContext.SaveChangesAsync(); await _cache.BroadcastRevoke(); diff --git a/SW.Bitween.Api/Resources/BusGateways/UpdateRoute.cs b/SW.Bitween.Api/Resources/BusGateways/UpdateRoute.cs index 5bd79f38..5cb4eeb1 100644 --- a/SW.Bitween.Api/Resources/BusGateways/UpdateRoute.cs +++ b/SW.Bitween.Api/Resources/BusGateways/UpdateRoute.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Domain.Gateway; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -23,7 +22,7 @@ public UpdateRoute(BitweenDbContext dbContext, RequestContext requestContext, II public async Task Handle(int gatewayId, BusGatewayRouteUpdate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.BusGateways.Edit); var gateway = await _dbContext.Set() .FirstOrDefaultAsync(bg => bg.Id == gatewayId); @@ -37,10 +36,16 @@ public async Task Handle(int gatewayId, BusGatewayRouteUpdate model) if (route == null) throw new SWNotFoundException($"Route with Id {model.RouteId} not found in gateway {gatewayId}"); - await AddRoute.ValidateSubscription(_dbContext, model.SubscriptionId, gateway.DocumentId); + // Repointing an existing route always names an integration that already exists; + // defining one inline is only for the route being created. + if (!model.SubscriptionId.HasValue) + throw new SWValidationException(GatewayLinkTarget.NeitherGiven, + "Pick the integration this route runs."); + + await AddRoute.ValidateSubscription(_dbContext, model.SubscriptionId.Value, gateway.DocumentId); await AddRoute.ValidatePartner(_dbContext, model.PartnerId); - route.SubscriptionId = model.SubscriptionId; + route.SubscriptionId = model.SubscriptionId.Value; route.PartnerId = model.PartnerId; route.MatchExpression = model.MatchExpression; diff --git a/SW.Bitween.Api/Resources/Dashboard/ChartDataPoints.cs b/SW.Bitween.Api/Resources/Dashboard/ChartDataPoints.cs index 8e13f439..6df53b00 100644 --- a/SW.Bitween.Api/Resources/Dashboard/ChartDataPoints.cs +++ b/SW.Bitween.Api/Resources/Dashboard/ChartDataPoints.cs @@ -11,16 +11,20 @@ namespace SW.Bitween.Resources.Dashboard; public class ChartsDataPoints : IQueryHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; private readonly DateTime _dataDateLimit; - public ChartsDataPoints(BitweenDbContext dbContext) + public ChartsDataPoints(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; _dataDateLimit = DateTime.UtcNow.AddMonths(-3); } public async Task Handle() { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Dashboard.View); + var xChangesPerDay = await _dbContext.Set() .AsNoTracking() .Where(i => i.StartedOn >= _dataDateLimit) diff --git a/SW.Bitween.Api/Resources/Dashboard/MainInfo.cs b/SW.Bitween.Api/Resources/Dashboard/MainInfo.cs index c819cffe..b2a36b4f 100644 --- a/SW.Bitween.Api/Resources/Dashboard/MainInfo.cs +++ b/SW.Bitween.Api/Resources/Dashboard/MainInfo.cs @@ -12,14 +12,18 @@ namespace SW.Bitween.Resources.Dashboard; public class MainInfo : IQueryHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; - public MainInfo(BitweenDbContext dbContext) + public MainInfo(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; } public async Task Handle() { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Dashboard.View); + var subscriptionsCount = await _dbContext.Set().AsNoTracking().CountAsync(); var documentCount = await _dbContext.Set().AsNoTracking().CountAsync(); var notifiersCount = await _dbContext.Set().AsNoTracking().CountAsync(); diff --git a/SW.Bitween.Api/Resources/Dashboard/XChangesAndSubscriptionsInfo.cs b/SW.Bitween.Api/Resources/Dashboard/XChangesAndSubscriptionsInfo.cs index f2eec15a..7214d584 100644 --- a/SW.Bitween.Api/Resources/Dashboard/XChangesAndSubscriptionsInfo.cs +++ b/SW.Bitween.Api/Resources/Dashboard/XChangesAndSubscriptionsInfo.cs @@ -14,6 +14,7 @@ namespace SW.Bitween.Resources.Dashboard; public class XChangesAndSubscriptionsInfo : IQueryHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; private readonly DateTime _dataDateLimit; private readonly XchangeService _xchangeService; @@ -21,9 +22,10 @@ public class XChangesAndSubscriptionsInfo : IQueryHandler // private readonly IMemoryCache _memoryCache; // private const string CACHE_KEY = "XChangesAndSubscriptionsInfoCache"; - public XChangesAndSubscriptionsInfo(BitweenDbContext dbContext, XchangeService xchangeService) + public XChangesAndSubscriptionsInfo(BitweenDbContext dbContext, XchangeService xchangeService, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; _xchangeService = xchangeService; //_memoryCache = memoryCache; _dataDateLimit = DateTime.UtcNow.AddMonths(-3); @@ -31,6 +33,8 @@ public XChangesAndSubscriptionsInfo(BitweenDbContext dbContext, XchangeService x public async Task Handle() { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Dashboard.View); + var totalXchangesCount = await _dbContext.Set().AsNoTracking().CountAsync(); var xChangeCountInTimeframe = await _dbContext.Set() .Where(i => i.StartedOn >= _dataDateLimit) diff --git a/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs b/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs index 9f287393..3efee3e6 100644 --- a/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs +++ b/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -23,14 +22,27 @@ public RunNow(BitweenDbContext dbContext, RequestContext requestContext, Xchange public async Task Handle(string key, DelayedRetryRunNow request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + // What's being operated on is an exchange, not the policy that scheduled the retry — + // and this is the same page the UI gates on exchange permissions. + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Exchanges.Operate); var delayedRetry = await _dbContext.Set().FirstOrDefaultAsync(d => d.Id == key); if (delayedRetry == null) throw new SWValidationException("NOT_FOUND", "No auto-retry is currently scheduled for this exchange."); if (!await _xchangeService.ExecuteDelayedRetry(delayedRetry)) - throw new SWValidationException("NOT_FOUND", "The original exchange or its subscription no longer exists."); + { + await _dbContext.SaveChangesAsync(); + + // Every other refusal writes its reason onto the exchange, so the message sends the + // caller there instead of listing them. A missing exchange is the one case with + // nowhere to write it, and pointing at something that is gone explains nothing. + var exchangeExists = await _dbContext.Set().AnyAsync(x => x.Id == key); + + throw new SWValidationException("CANNOT_RETRY", exchangeExists + ? "This retry could not be carried out. The exchange it belongs to says why." + : "This retry could not be carried out: the exchange it belonged to no longer exists."); + } await _dbContext.SaveChangesAsync(); return null; diff --git a/SW.Bitween.Api/Resources/DelayedRetries/Search.cs b/SW.Bitween.Api/Resources/DelayedRetries/Search.cs index 4925351c..a0b94699 100644 --- a/SW.Bitween.Api/Resources/DelayedRetries/Search.cs +++ b/SW.Bitween.Api/Resources/DelayedRetries/Search.cs @@ -11,18 +11,29 @@ namespace SW.Bitween.Resources.DelayedRetries public class Search : ISearchyHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; } public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Exchanges.View, Model.Permissions.Dashboard.View); + var query = from delayedRetry in _dbContext.Set() join xchange in _dbContext.Set() on delayedRetry.Id equals xchange.Id join result in _dbContext.Set() on xchange.Id equals result.Id into xr from result in xr.DefaultIfEmpty() + // Same left-join Xchanges/Search.cs uses — the UI lists a pending retry by + // what it carries, not by its id, so the properties have to come along. + join promoted in _dbContext.Set() on xchange.Id equals promoted.Id into xp + from promoted in xp.DefaultIfEmpty() join document in _dbContext.Set() on xchange.DocumentId equals document.Id join subscriber in _dbContext.Set() on xchange.SubscriptionId equals subscriber.Id into xs from subscriber in xs.DefaultIfEmpty() @@ -35,7 +46,10 @@ from subscriber in xs.DefaultIfEmpty() DocumentId = xchange.DocumentId, DocumentName = document.Name, Exception = result.Exception, - StartedOn = xchange.StartedOn + StartedOn = xchange.StartedOn, + PromotedProperties = promoted == null ? null : promoted.Properties.ToDictionary(), + RetryPolicyId = subscriber.RetryPolicyId, + RetryPolicyName = subscriber.RetryPolicy.Name }; query = query.OrderBy(r => r.On).AsNoTracking(); diff --git a/SW.Bitween.Api/Resources/Documents/Create.cs b/SW.Bitween.Api/Resources/Documents/Create.cs index 033e9cd9..87b791f2 100644 --- a/SW.Bitween.Api/Resources/Documents/Create.cs +++ b/SW.Bitween.Api/Resources/Documents/Create.cs @@ -1,4 +1,5 @@ using FluentValidation; +using Microsoft.EntityFrameworkCore; using SW.EfCoreExtensions; using SW.Bitween.Domain; using SW.Bitween.Model; @@ -8,7 +9,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Documents { @@ -16,22 +16,75 @@ public class Create : ICommandHandler { private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; + private readonly IBroadcast _broadcast; - public Create(BitweenDbContext dbContext, RequestContext requestContext) + public Create(BitweenDbContext dbContext, RequestContext requestContext, IBroadcast broadcast) { _dbContext = dbContext; _requestContext = requestContext; + _broadcast = broadcast; } public async Task Handle(DocumentCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Documents.Create); - var entity = new Document(model.Id, model.Name, model.DocumentFormat); + // Same check Update makes, and compared the same way. Without it two types + // could be created under one name, and then neither could be saved again — + // Update refuses the name it already has. Ignoring case, because a list + // holding both "Invoice" and "invoice" reads as a mistake, not a choice. + var wantedName = (model.Name ?? string.Empty).ToLower(); + if (await _dbContext.Set().AsNoTracking().AnyAsync(d => d.Name.ToLower() == wantedName)) + throw new SWValidationException("NAME_TAKEN", "An information type with this name already exists."); + + var code = string.IsNullOrWhiteSpace(model.Code) ? null : model.Code; + + if (code != null && await _dbContext.Set().AsNoTracking().AnyAsync(d => d.Code == code)) + throw new SWValidationException("CODE_TAKEN", "This code is already in use."); + + if (model.BusEnabled && !string.IsNullOrEmpty(model.BusMessageTypeName)) + { + // Compared lower-cased, because that is how the bus compares them: both + // BasicPublisher and ConsumerDefinition derive the routing key with + // ToLower(), so "Foo" and "foo" are one message on the wire. Matching + // exactly here let both exist, and then every message published under + // either name reached both gateways, silently. ToLower() rather than a + // provider-specific collation — this runs on Postgres, MySql and MsSql. + var wanted = model.BusMessageTypeName.ToLower(); + var busTypeNameDuplicated = await _dbContext.Set() + .AsNoTracking() + .AnyAsync(d => d.BusMessageTypeName.ToLower() == wanted); + if (busTypeNameDuplicated) + throw new SWValidationException("DUPLICATED_BUS_TYPE_NAME", + $"Another information type already publishes as '{model.BusMessageTypeName}'. " + + "Names are compared ignoring case, because the bus does."); + } + + PromotedPropertyValidation.Check(model.PromotedProperties, model.DocumentFormat); + + var entity = new Document(code, model.Name, model.DocumentFormat) + { + BusEnabled = model.BusEnabled, + BusMessageTypeName = model.BusEnabled ? model.BusMessageTypeName : null, + DuplicateInterval = model.DuplicateInterval, + DisregardsUnfilteredMessages = model.DisregardsUnfilteredMessages, + }; + if (model.PromotedProperties != null) + entity.SetDictionaries(model.PromotedProperties.ToDictionary()); + + // After the entity is complete: the trail serialises it in the constructor + // when isNew, so anything set later would be missing from the created state. var trail = new DocumentTrail(DocumentTrailCode.Created, entity, true); _dbContext.Add(trail); _dbContext.Add(entity); await _dbContext.SaveChangesAsync(); + + // A bus-enabled type adds a queue, and the consumer set is only rebuilt when asked. + // Without this the queue is declared but nothing ever consumes it, until either an + // unrelated document update happens to refresh consumers or the app restarts. + if (entity.BusEnabled) + await _broadcast.RefreshConsumers(); + return entity.Id; } @@ -39,8 +92,15 @@ private class Validate : AbstractValidator { public Validate() { - RuleFor(i => i.Id).NotEmpty(); + RuleFor(i => i.Code) + .Matches("^[A-Z][A-Z0-9_]{1,49}$") + .When(i => !string.IsNullOrEmpty(i.Code)) + .WithMessage("Codes are upper-case letters, digits and underscores (2-50 chars)."); RuleFor(i => i.Name).NotEmpty(); + RuleFor(i => i.BusMessageTypeName) + .Matches("^\\S+$") + .When(i => !string.IsNullOrEmpty(i.BusMessageTypeName)) + .WithMessage("Bus message type name cannot contain spaces."); } } } diff --git a/SW.Bitween.Api/Resources/Documents/Delete.cs b/SW.Bitween.Api/Resources/Documents/Delete.cs index 2749201d..cddf1571 100644 --- a/SW.Bitween.Api/Resources/Documents/Delete.cs +++ b/SW.Bitween.Api/Resources/Documents/Delete.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Documents { @@ -22,7 +21,7 @@ public Delete(BitweenDbContext dbContext, RequestContext requestContext) async public Task Handle(int key) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Documents.Delete); await _dbContext.DeleteByKeyAsync(key); return null; diff --git a/SW.Bitween.Api/Resources/Documents/Get.cs b/SW.Bitween.Api/Resources/Documents/Get.cs index cbabd883..3b0a9885 100644 --- a/SW.Bitween.Api/Resources/Documents/Get.cs +++ b/SW.Bitween.Api/Resources/Documents/Get.cs @@ -14,17 +14,22 @@ namespace SW.Bitween.Resources.Documents public class Get : IGetHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public Get(BitweenDbContext dbContext) + public Get(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } public async Task Handle(int key) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Documents.View); + return await dbContext.Set().Search("Id", key).Select(document => new DocumentUpdate { Id = document.Id, + Code = document.Code, Name = document.Name, BusEnabled = document.BusEnabled, BusMessageTypeName = document.BusMessageTypeName, diff --git a/SW.Bitween.Api/Resources/Documents/GetProperties.cs b/SW.Bitween.Api/Resources/Documents/GetProperties.cs index afc108fd..acb65f24 100644 --- a/SW.Bitween.Api/Resources/Documents/GetProperties.cs +++ b/SW.Bitween.Api/Resources/Documents/GetProperties.cs @@ -12,14 +12,18 @@ namespace SW.Bitween.Resources.Documents public class GetProperties : IGetHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public GetProperties(BitweenDbContext dbContext) + public GetProperties(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } async public Task Handle(int key) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Documents.View); + var document = await dbContext.FindAsync(key); return document.PromotedProperties.ToDictionary(k => k.Key, v => v.Key); } diff --git a/SW.Bitween.Api/Resources/Documents/GetTrail.cs b/SW.Bitween.Api/Resources/Documents/GetTrail.cs index 19b02d8e..63cd6932 100644 --- a/SW.Bitween.Api/Resources/Documents/GetTrail.cs +++ b/SW.Bitween.Api/Resources/Documents/GetTrail.cs @@ -14,15 +14,19 @@ namespace SW.Bitween.Resources.Documents; public class GetTrail : IQueryHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public GetTrail(BitweenDbContext dbContext) + public GetTrail(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } public async Task Handle(SearchDocumentTrailModel request) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Documents.View); + request.Limit ??= 20; request.Offset ??= 0; var trails = dbContext.Set() diff --git a/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs b/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs new file mode 100644 index 00000000..0be807c3 --- /dev/null +++ b/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Documents +{ + /// + /// The rules a promoted property has to satisfy, shared by Create and Update so + /// the two paths cannot drift apart on what a valid path is. + /// + public static class PromotedPropertyValidation + { + public static void Check(ICollection promotedProperties, DocumentFormat format) + { + if (promotedProperties == null) return; + + foreach (var pp in promotedProperties) + { + if (string.IsNullOrWhiteSpace(pp.Key)) + throw new SWValidationException("INVALID_PROMOTED_PROPERTY_KEY", + "Promoted property key cannot be null or empty."); + + if (string.IsNullOrWhiteSpace(pp.Value)) + throw new SWValidationException("INVALID_PROMOTED_PROPERTY_VALUE", + $"Promoted property '{pp.Key}' must have a non-empty path value."); + + var trimmed = pp.Value.Trim(); + + if (format == DocumentFormat.Json) + { + // Must be a JSONPath: starts with '$' or a simple dot-separated identifier path + if (!trimmed.StartsWith("$") && !Regex.IsMatch(trimmed, @"^[a-zA-Z_][a-zA-Z0-9_]*(?:(\.[a-zA-Z_][a-zA-Z0-9_]*)|(\[[0-9]+\]))*$")) + throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", + $"Promoted property '{pp.Key}' has an invalid JSON path: '{pp.Value}'. Expected a JSONPath expression (e.g. '$.field.subField') or dot-notation path."); + } + else if (format == DocumentFormat.Xml) + { + // Basic XPath sanity: must start with '/' or '//' or be a valid element path + if (!trimmed.StartsWith("/") && !Regex.IsMatch(trimmed, @"^[a-zA-Z_][a-zA-Z0-9_/\[\]@.:*-]*$")) + throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", + $"Promoted property '{pp.Key}' has an invalid XML path: '{pp.Value}'. Expected an XPath expression (e.g. '/root/element')."); + } + } + + var duplicateKey = promotedProperties + .GroupBy(pp => pp.Key, System.StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(g => g.Count() > 1)?.Key; + + if (duplicateKey != null) + throw new SWValidationException("DUPLICATE_PROMOTED_PROPERTY_KEY", + $"Promoted property key '{duplicateKey}' appears more than once."); + } + } +} diff --git a/SW.Bitween.Api/Resources/Documents/Search.cs b/SW.Bitween.Api/Resources/Documents/Search.cs index 77823590..1b2938f4 100644 --- a/SW.Bitween.Api/Resources/Documents/Search.cs +++ b/SW.Bitween.Api/Resources/Documents/Search.cs @@ -14,19 +14,27 @@ namespace SW.Bitween.Resources.Documents public class Search : ISearchyHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } async public Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await requestContext.EnsurePermission(dbContext, Model.Permissions.Documents.View); + var query = from document in dbContext.Set() select new DocumentRow { Id = document.Id, + Code = document.Code, Name = document.Name, BusMessageTypeName = document.BusMessageTypeName, BusEnabled = document.BusEnabled, diff --git a/SW.Bitween.Api/Resources/Documents/Update.cs b/SW.Bitween.Api/Resources/Documents/Update.cs index c899d93c..a30b8829 100644 --- a/SW.Bitween.Api/Resources/Documents/Update.cs +++ b/SW.Bitween.Api/Resources/Documents/Update.cs @@ -5,7 +5,6 @@ using SW.PrimitiveTypes; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using SW.Bitween.Domain.Accounts; using System.Text.RegularExpressions; namespace SW.Bitween.Resources.Documents @@ -29,62 +28,69 @@ public Update(BitweenDbContext dbContext, IInfolinkCache BitweenCache, RequestCo public async Task Handle(int key, DocumentUpdate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Documents.Edit); var entity = await _dbContext.FindAsync(key); + if (string.IsNullOrWhiteSpace(model.Name)) + throw new SWValidationException("INVALID_NAME", "Give the information type a name."); + + // Ignoring case, as Create does: two types whose names differ only in case + // are indistinguishable in every list that shows them. + var wantedName = model.Name.ToLower(); + var nameDuplicated = await _dbContext.Set() + .AsNoTracking() + .Where(i => i.Id != key) + .AnyAsync(i => i.Name.ToLower() == wantedName); + if (nameDuplicated) + throw new SWValidationException("NAME_TAKEN", "An information type with this name already exists."); + + var code = string.IsNullOrWhiteSpace(model.Code) ? null : model.Code; + + if (code != null && !Regex.IsMatch(code, "^[A-Z][A-Z0-9_]{1,49}$")) + throw new SWValidationException("INVALID_CODE", + "Codes are upper-case letters, digits and underscores (2-50 chars)."); + + if (code != null) + { + var codeDuplicated = await _dbContext.Set() + .AsNoTracking() + .Where(i => i.Id != key) + .AnyAsync(i => i.Code == code); + if (codeDuplicated) + throw new SWValidationException("CODE_TAKEN", "This code is already in use."); + } + + if (!string.IsNullOrEmpty(model.BusMessageTypeName) && Regex.IsMatch(model.BusMessageTypeName, @"\s")) + throw new SWValidationException("INVALID_BUS_TYPE_NAME", + "Bus message type name cannot contain spaces."); + + // Ignoring case, for the reason spelled out in Create: the routing key is + // lower-cased at both ends, so two names differing only in case are one message. + var wanted = (model.BusMessageTypeName ?? string.Empty).ToLower(); var busTypeNameDuplicated = await _dbContext.Set() .AsNoTracking() .Where(i => i.Id != key) .Where(i => !string.IsNullOrEmpty(i.BusMessageTypeName)) - .Where(i => i.BusMessageTypeName == model.BusMessageTypeName) + .Where(i => i.BusMessageTypeName.ToLower() == wanted) .AnyAsync(); if (busTypeNameDuplicated) throw new SWValidationException("DUPLICATED_BUS_TYPE_NAME", - "Cant use duplicated bus Message type name"); + $"Another information type already publishes as '{model.BusMessageTypeName}'. " + + "Names are compared ignoring case, because the bus does."); - if (model.PromotedProperties != null) - { - foreach (var pp in model.PromotedProperties) - { - if (string.IsNullOrWhiteSpace(pp.Key)) - throw new SWValidationException("INVALID_PROMOTED_PROPERTY_KEY", - "Promoted property key cannot be null or empty."); - - if (string.IsNullOrWhiteSpace(pp.Value)) - throw new SWValidationException("INVALID_PROMOTED_PROPERTY_VALUE", - $"Promoted property '{pp.Key}' must have a non-empty path value."); - - if (model.DocumentFormat == DocumentFormat.Json) - { - // Must be a JSONPath: starts with '$' or a simple dot-separated identifier path - var trimmed = pp.Value.Trim(); - if (!trimmed.StartsWith("$") && !Regex.IsMatch(trimmed, @"^[a-zA-Z_][a-zA-Z0-9_]*(?:(\.[a-zA-Z_][a-zA-Z0-9_]*)|(\[[0-9]+\]))*$")) - throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", - $"Promoted property '{pp.Key}' has an invalid JSON path: '{pp.Value}'. Expected a JSONPath expression (e.g. '$.field.subField') or dot-notation path."); - } - else if (model.DocumentFormat == DocumentFormat.Xml) - { - // Basic XPath sanity: must start with '/' or '//' or be a valid element path - var trimmed = pp.Value.Trim(); - if (!trimmed.StartsWith("/") && !Regex.IsMatch(trimmed, @"^[a-zA-Z_][a-zA-Z0-9_/\[\]@.:*-]*$")) - throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", - $"Promoted property '{pp.Key}' has an invalid XML path: '{pp.Value}'. Expected an XPath expression (e.g. '/root/element')."); - } - } - - var duplicateKey = model.PromotedProperties - .GroupBy(pp => pp.Key, System.StringComparer.OrdinalIgnoreCase) - .FirstOrDefault(g => g.Count() > 1)?.Key; - - if (duplicateKey != null) - throw new SWValidationException("DUPLICATE_PROMOTED_PROPERTY_KEY", - $"Promoted property key '{duplicateKey}' appears more than once."); - } + PromotedPropertyValidation.Check(model.PromotedProperties, model.DocumentFormat); var trail = new DocumentTrail(DocumentTrailCode.Updated, entity); - entity.SetDictionaries(model.PromotedProperties.ToDictionary()); + // An absent list means none, the same as it does for retry policy groups. + // Left implicit it threw ArgumentNullException — a 500 for a request the + // API had simply never decided the meaning of. + entity.SetDictionaries((model.PromotedProperties ?? []).ToDictionary()); + // Name/Code have private setters — SetProperties only writes public-setter + // properties, so it silently no-ops on these two (verified empirically). + entity.SetName(model.Name); + entity.SetCode(code); _dbContext.Entry(entity).SetProperties(model); trail.SetAfter(entity); diff --git a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Create.cs b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Create.cs index 6d131146..551bc716 100644 --- a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Create.cs +++ b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Create.cs @@ -2,7 +2,6 @@ using FluentValidation; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -21,7 +20,7 @@ public Create(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(GlobalAdapterValuesSetCreate request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.GlobalValues.Create); var exists = await _dbContext.Set().AnyAsync(x => x.Id == request.Id); if (exists) diff --git a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Delete.cs b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Delete.cs index 6a8e557a..40a20d97 100644 --- a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Delete.cs +++ b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Delete.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -20,7 +19,7 @@ public Delete(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(string key, DeleteGlobalAdapterValuesSetModel _) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.GlobalValues.Delete); var entity = await _dbContext.Set().FindAsync(key); if (entity is null) diff --git a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Get.cs b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Get.cs index 57a229e1..186f516f 100644 --- a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Get.cs +++ b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Get.cs @@ -19,7 +19,7 @@ public Get(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(string key) { - _requestContext.EnsureAccess(Domain.Accounts.AccountRole.Admin, Domain.Accounts.AccountRole.Member, Domain.Accounts.AccountRole.Viewer); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.GlobalValues.View); var entity = await _dbContext.Set() .AsNoTracking() diff --git a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Search.cs b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Search.cs index 9f3d1f73..15771b15 100644 --- a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Search.cs +++ b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Search.cs @@ -11,14 +11,21 @@ namespace SW.Bitween.Resources.GlobalAdapterValuesSets public class Search : ISearchyHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; } public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.GlobalValues.View); + var query = from item in _dbContext.Set() select new GlobalAdapterValuesSetRow { diff --git a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Update.cs b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Update.cs index 43090d1b..46377ad0 100644 --- a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Update.cs +++ b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Update.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using FluentValidation; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -20,7 +19,7 @@ public Update(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(string key, GlobalAdapterValuesSetUpdate request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.GlobalValues.Edit); var entity = await _dbContext.Set().FindAsync(key); if (entity is null) diff --git a/SW.Bitween.Api/Resources/Login/Login.cs b/SW.Bitween.Api/Resources/Login/Login.cs index b5aa25ea..b823592a 100644 --- a/SW.Bitween.Api/Resources/Login/Login.cs +++ b/SW.Bitween.Api/Resources/Login/Login.cs @@ -31,9 +31,13 @@ public Task Handle(UserLogin request) { if (cred[1].Equals(request.Password)) { + // No account backs these configured credentials, so there are no roles to + // resolve — grant everything explicitly rather than relying on a guard that + // fails open on a missing claim. var claims = new List { new Claim(ClaimTypes.Name, cred[0]), + new Claim(RequestContextExtensions.SuperuserClaim, "true"), }; return Task.FromResult(new diff --git a/SW.Bitween.Api/Resources/Notifications/Search.cs b/SW.Bitween.Api/Resources/Notifications/Search.cs index cba67ac1..86052aff 100644 --- a/SW.Bitween.Api/Resources/Notifications/Search.cs +++ b/SW.Bitween.Api/Resources/Notifications/Search.cs @@ -11,14 +11,21 @@ namespace SW.Bitween.Resources.Notifications public class Search:ISearchyHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await requestContext.EnsurePermission(dbContext, Model.Permissions.Notifiers.View); + var query = from notification in dbContext.Set() select new NotificationsSearch() { diff --git a/SW.Bitween.Api/Resources/Notifiers/Create.cs b/SW.Bitween.Api/Resources/Notifiers/Create.cs index d117784d..bc6bb088 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Create.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Create.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using FluentValidation; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -20,7 +19,7 @@ public Create(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(NotifierCreate request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Notifiers.Create); var notifier = new Notifier(request.Name); diff --git a/SW.Bitween.Api/Resources/Notifiers/Delete.cs b/SW.Bitween.Api/Resources/Notifiers/Delete.cs new file mode 100644 index 00000000..611e0eb9 --- /dev/null +++ b/SW.Bitween.Api/Resources/Notifiers/Delete.cs @@ -0,0 +1,32 @@ +using SW.EfCoreExtensions; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; +using System.Threading.Tasks; + +namespace SW.Bitween.Resources.Notifiers +{ + public class Delete : IDeleteHandler + { + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Delete(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + /// + /// No reference check, unlike an integration's delete: nothing has a foreign key to a + /// notifier. RunOnSubscriptions points the other way — the notifier names the + /// integrations it watches, so deleting it takes the whole list with it. + /// + public async Task Handle(int key) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Notifiers.Delete); + + await _dbContext.DeleteByKeyAsync(key); + return null; + } + } +} diff --git a/SW.Bitween.Api/Resources/Notifiers/Get.cs b/SW.Bitween.Api/Resources/Notifiers/Get.cs index f675ed34..6f53ad20 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Get.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Get.cs @@ -11,14 +11,18 @@ namespace SW.Bitween.Resources.Notifiers public class Get: IGetHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public Get(BitweenDbContext dbContext) + public Get(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } public async Task Handle(int key) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Notifiers.View); + var notifier = await dbContext.Set().FirstOrDefaultAsync(n => n.Id == key); if (notifier == null) throw new SWNotFoundException(); diff --git a/SW.Bitween.Api/Resources/Notifiers/Search.cs b/SW.Bitween.Api/Resources/Notifiers/Search.cs index 7752ceac..b1dad100 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Search.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Search.cs @@ -11,14 +11,21 @@ namespace SW.Bitween.Resources.Notifiers public class Search: ISearchyHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await requestContext.EnsurePermission(dbContext, Model.Permissions.Notifiers.View); + var query = from notifier in dbContext.Set() select new NotifierSearch() { @@ -28,7 +35,8 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa RunOnBadResult = notifier.RunOnBadResult, RunOnFailedResult = notifier.RunOnFailedResult, RunOnSuccessfulResult = notifier.RunOnSuccessfulResult, - Inactive = notifier.Inactive + Inactive = notifier.Inactive, + RunOnSubscriptions = notifier.RunOnSubscriptions }; query = query.AsNoTracking(); diff --git a/SW.Bitween.Api/Resources/Notifiers/Update.cs b/SW.Bitween.Api/Resources/Notifiers/Update.cs index 44cf96af..302f4c83 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Update.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Update.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using FluentValidation; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -21,7 +20,7 @@ public Update(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key, NotifierUpdate request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Notifiers.Edit); var notifier = await _dbContext.FindAsync(key); @@ -32,7 +31,9 @@ public async Task Handle(int key, NotifierUpdate request) request.Inactive, request.RunOnSubscriptions?.Select(r => r.Id)?.ToArray()); - notifier.SetDictionaries(request.HandlerProperties.ToDictionary()); + // An absent list means none, as it does for a document's promoted properties + // and a retry policy's groups. Left implicit it threw ArgumentNullException. + notifier.SetDictionaries((request.HandlerProperties ?? []).ToDictionary()); await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/Ops/Alerts.cs b/SW.Bitween.Api/Resources/Ops/Alerts.cs index 417cea4d..f48ba65f 100644 --- a/SW.Bitween.Api/Resources/Ops/Alerts.cs +++ b/SW.Bitween.Api/Resources/Ops/Alerts.cs @@ -1,14 +1,18 @@ using System.Threading.Tasks; +using SW.Bitween.Domain; using SW.Bus.RabbitMqExtensions; using SW.PrimitiveTypes; namespace SW.Bitween.Resources.Ops; [HandlerName("Alerts")] -public class Alerts(IBusDashboardDataService dashboardDataService) : IQueryHandler +public class Alerts(IBusDashboardDataService dashboardDataService, + BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler { public async Task Handle() { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View, Model.Permissions.Dashboard.View); + return await dashboardDataService.GetAlertsAsync(); } } diff --git a/SW.Bitween.Api/Resources/Ops/Consumers.cs b/SW.Bitween.Api/Resources/Ops/Consumers.cs index 72b9a4cf..3e2b84ea 100644 --- a/SW.Bitween.Api/Resources/Ops/Consumers.cs +++ b/SW.Bitween.Api/Resources/Ops/Consumers.cs @@ -1,14 +1,72 @@ +using System.Linq; using System.Threading.Tasks; +using SW.Bitween.Domain; using SW.Bus.RabbitMqExtensions; using SW.PrimitiveTypes; namespace SW.Bitween.Resources.Ops; +/// +/// The bus's consumer health, with each row told what it is for. Without the lane and the +/// ids a caller can only see the queue's machine name, and would have to rebuild +/// for itself to work out which group a row is. +/// +public record ConsumerLaneView( + string Name, + string MessageName, + string QueueName, + string Lane, + string Title, + int? WorkGroupId, + int? InformationTypeId, + long TotalNodes, + long ProcessingCount, + long QueueCount, + long RetryCount, + long FailedCount, + int Priority, + ushort Prefetch, + double IncomingRate, + double ProcessingRate, + double AckRate, + bool IsBackpressured, + AlertSeverity HealthStatus); + [HandlerName("Consumers")] -public class Consumers(IBusDashboardDataService dashboardDataService) : IQueryHandler +public class Consumers(IBusDashboardDataService dashboardDataService, + LaneResolver laneResolver, + BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler { public async Task Handle() { - return await dashboardDataService.GetConsumerHealthAsync(); + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View, Model.Permissions.Dashboard.View); + + var health = await dashboardDataService.GetConsumerHealthAsync(); + await laneResolver.Prepare(); + + return health.Select(c => + { + var identity = laneResolver.Resolve(c.Name, c.MessageName); + return new ConsumerLaneView( + c.Name, + c.MessageName, + c.QueueName, + identity.Lane.ToString(), + identity.Title, + identity.WorkGroupId, + identity.InformationTypeId, + c.TotalNodes, + c.ProcessingCount, + c.QueueCount, + c.RetryCount, + c.FailedCount, + c.Priority, + c.Prefetch, + c.IncomingRate, + c.ProcessingRate, + c.AckRate, + c.IsBackpressured, + c.HealthStatus); + }).ToArray(); } } diff --git a/SW.Bitween.Api/Resources/Ops/DeadLetters.cs b/SW.Bitween.Api/Resources/Ops/DeadLetters.cs index 39a790ce..f9c0d183 100644 --- a/SW.Bitween.Api/Resources/Ops/DeadLetters.cs +++ b/SW.Bitween.Api/Resources/Ops/DeadLetters.cs @@ -1,14 +1,18 @@ using System.Threading.Tasks; +using SW.Bitween.Domain; using SW.Bus.RabbitMqExtensions; using SW.PrimitiveTypes; namespace SW.Bitween.Resources.Ops; [HandlerName("DeadLetters")] -public class DeadLetters(IBusDashboardDataService dashboardDataService) : IQueryHandler +public class DeadLetters(IBusDashboardDataService dashboardDataService, + BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler { public async Task Handle() { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View, Model.Permissions.Dashboard.View); + return await dashboardDataService.GetDeadLetterSummaryAsync(); } } diff --git a/SW.Bitween.Api/Resources/Ops/LaneResolver.cs b/SW.Bitween.Api/Resources/Ops/LaneResolver.cs new file mode 100644 index 00000000..11bcab58 --- /dev/null +++ b/SW.Bitween.Api/Resources/Ops/LaneResolver.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.EfCoreExtensions; + +namespace SW.Bitween.Resources.Ops; + +/// +/// What a consumed queue is for. The bus names queues after the consumer class and the +/// bus message type — xchangeservice.48low-result — and only Bitween knows that +/// 48low is work group 48. Resolving it here keeps the answer next to +/// , which is the formula's owner; the UI used +/// to rebuild that formula in TypeScript and match on the string. +/// +public enum QueueLane +{ + /// One per bus-enabled information type. Inbound messages become exchanges. + FrontDoor, + + /// One per work group. Where integrations run. + Work, + + /// A work group's -Result lane. Evaluates notifiers. + Notifications, + + /// A legacy event type, consumed only while that setting is on. + Legacy, + + /// Bookkeeping. No integration traffic. + Control, +} + +/// What the queue is for. +/// The name of the thing it belongs to, or the raw message name if that no longer resolves. +/// Set on a work or notifications lane whose group still exists. +/// Set on a front door whose information type still exists. +public record LaneIdentity(QueueLane Lane, string Title, int? WorkGroupId, int? InformationTypeId); + +/// +/// Decodes the message-type half of a queue name into the thing it belongs to. One database +/// read for all rows, so callers should resolve a whole snapshot at once. +/// +public class LaneResolver(BitweenDbContext dbContext) +{ + /// 's non-lane message types. + private static readonly Dictionary Control = new(System.StringComparer.OrdinalIgnoreCase) + { + ["SubscriptionUnpausedEvent"] = "Resumed integrations", + }; + + /// Only declared while Bitween.ConsumeLegacyEventMessages is on. + private static readonly Dictionary Legacy = new(System.StringComparer.OrdinalIgnoreCase) + { + ["ApiXchangeCreatedEvent"] = "API exchange created", + ["InternalXchangeCreatedEvent"] = "Internal exchange created", + ["ReceivingXchangeCreatedEvent"] = "Received exchange created", + ["AggregateXchangeCreatedEvent"] = "Aggregated exchange created", + ["XchangeResultCreatedEvent"] = "Exchange result created", + }; + + private List workGroups; + private List documents; + + public async Task Prepare() + { + workGroups = await dbContext.Set().AsNoTracking().ToListAsync(); + documents = (await dbContext.ListAsync(new BusEnabledDocuments())).ToList(); + } + + /// The consumer class name, as the bus reports it. + /// The bus message type name. + public LaneIdentity Resolve(string consumerName, string messageName) + { + if (consumerName == nameof(BusService)) + { + var document = documents.FirstOrDefault(d => + string.Equals(d.BusMessageTypeName, messageName, System.StringComparison.OrdinalIgnoreCase)); + + // A bus message only gets a queue while its information type is bus-enabled, so a + // name that resolves to nothing means the queue outlived it — and queues are never + // deleted. Reported as-is rather than hidden. + return new LaneIdentity(QueueLane.FrontDoor, document?.Name ?? messageName, null, document?.Id); + } + + // Tolerate a consumer class this hasn't been taught rather than guessing it is a lane. + if (consumerName != nameof(XchangeService)) + return new LaneIdentity(QueueLane.Control, $"{consumerName} · {messageName}", null, null); + + if (Control.TryGetValue(messageName, out var control)) + return new LaneIdentity(QueueLane.Control, control, null, null); + + if (Legacy.TryGetValue(messageName, out var legacy)) + return new LaneIdentity(QueueLane.Legacy, legacy, null, null); + + var isResult = messageName.EndsWith(XchangeService.ResultQueueSuffix, System.StringComparison.OrdinalIgnoreCase); + var lane = isResult ? QueueLane.Notifications : QueueLane.Work; + var groupName = isResult + ? messageName[..^XchangeService.ResultQueueSuffix.Length] + : messageName; + + // WorkGroup.None — id 0, so it can never collide with a real group. This is the lane + // everything without a work group shares. + if (string.Equals(groupName, WorkGroup.None.GetBusMessageName(), System.StringComparison.OrdinalIgnoreCase)) + return new LaneIdentity(lane, "Ungrouped", null, null); + + var workGroup = workGroups.FirstOrDefault(wg => + string.Equals(wg.GetBusMessageName(), groupName, System.StringComparison.OrdinalIgnoreCase)); + + return new LaneIdentity(lane, workGroup?.Name ?? messageName, workGroup?.Id, null); + } +} diff --git a/SW.Bitween.Api/Resources/Ops/Queues.cs b/SW.Bitween.Api/Resources/Ops/Queues.cs index 6ebb2f34..a12cb4ae 100644 --- a/SW.Bitween.Api/Resources/Ops/Queues.cs +++ b/SW.Bitween.Api/Resources/Ops/Queues.cs @@ -1,14 +1,18 @@ using System.Threading.Tasks; +using SW.Bitween.Domain; using SW.Bus.RabbitMqExtensions; using SW.PrimitiveTypes; namespace SW.Bitween.Resources.Ops; [HandlerName("Queues")] -public class Queues(IBusDashboardDataService dashboardDataService) : IQueryHandler +public class Queues(IBusDashboardDataService dashboardDataService, + BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler { public async Task Handle() { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View, Model.Permissions.Dashboard.View); + return await dashboardDataService.GetQueueDetailsAsync(); } } diff --git a/SW.Bitween.Api/Resources/Ops/Retries.cs b/SW.Bitween.Api/Resources/Ops/Retries.cs index 2f08bd75..0300d67d 100644 --- a/SW.Bitween.Api/Resources/Ops/Retries.cs +++ b/SW.Bitween.Api/Resources/Ops/Retries.cs @@ -1,14 +1,18 @@ using System.Threading.Tasks; +using SW.Bitween.Domain; using SW.Bus.RabbitMqExtensions; using SW.PrimitiveTypes; namespace SW.Bitween.Resources.Ops; [HandlerName("Retries")] -public class Retries(IBusDashboardDataService dashboardDataService) : IQueryHandler +public class Retries(IBusDashboardDataService dashboardDataService, + BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler { public async Task Handle() { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View, Model.Permissions.Dashboard.View); + return await dashboardDataService.GetRetryAnalysisAsync(); } } diff --git a/SW.Bitween.Api/Resources/Ops/Summary.cs b/SW.Bitween.Api/Resources/Ops/Summary.cs index ae4a30bb..538291c1 100644 --- a/SW.Bitween.Api/Resources/Ops/Summary.cs +++ b/SW.Bitween.Api/Resources/Ops/Summary.cs @@ -1,14 +1,18 @@ using System.Threading.Tasks; +using SW.Bitween.Domain; using SW.Bus.RabbitMqExtensions; using SW.PrimitiveTypes; namespace SW.Bitween.Resources.Ops; [HandlerName("Summary")] -public class Summary(IBusDashboardDataService dashboardDataService) : IQueryHandler +public class Summary(IBusDashboardDataService dashboardDataService, + BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler { public async Task Handle() { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View, Model.Permissions.Dashboard.View); + return await dashboardDataService.GetSummaryAsync(); } } diff --git a/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs b/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs new file mode 100644 index 00000000..d231c869 --- /dev/null +++ b/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using EasyNetQ.Management.Client; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Domain; +using SW.Bus; +using SW.Bus.RabbitMqExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Ops; + +/// The main queue as RabbitMQ has it. +/// Messages sitting in the main queue. +/// Messages in its .retry queue, if it has one. +/// Messages in its .bad queue, if it has one. +/// How many queues this lane is (main plus whichever of retry/bad exist). +public record UnattendedQueueView( + string QueueName, + long Messages, + long RetryMessages, + long DeadMessages, + int Queues); + +/// +/// Queues that exist in RabbitMQ under this instance's prefix that nothing here consumes. +/// +/// Every other Ops endpoint derives its list from the consumer definitions of the running +/// process, so it can only ever show queues this instance already knows about. Deleting or +/// renaming a work group leaves its queues behind — the bus never removes a queue — and they +/// vanish from those endpoints while keeping whatever they still hold. This is the only view +/// that asks the broker instead of asking ourselves. +/// +/// +[HandlerName("UnattendedQueues")] +public class UnattendedQueues(IBusDashboardDataService dashboardDataService, + BusOptions busOptions, + IMemoryCache memoryCache, + BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler +{ + public async Task Handle() + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View, Model.Permissions.Dashboard.View); + + var prefix = string.IsNullOrWhiteSpace(busOptions.ApplicationName) + ? busOptions.ProcessExchange + : $"{busOptions.ProcessExchange}.{busOptions.ApplicationName}"; + + // Same three names per consumer the bus itself declares. + var health = await dashboardDataService.GetConsumerHealthAsync(); + var attended = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var consumer in health) + { + attended.Add(consumer.QueueName); + attended.Add($"{consumer.QueueName}.retry"); + attended.Add($"{consumer.QueueName}.bad"); + } + + // Cached on the same clock as the bus's own management call, because this page polls. + var queues = await memoryCache.GetOrCreateAsync("bitween-all-queues", async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(busOptions.MonitoringCacheSeconds); + var client = new ManagementClient(new Uri(busOptions.ManagementUrl), + busOptions.ManagementUsername, busOptions.ManagementPassword); + return await client.GetQueuesAsync(busOptions.VirtualHost); + }); + + var orphans = queues + .Where(q => q.Name.StartsWith($"{prefix}.", StringComparison.OrdinalIgnoreCase)) + .Where(q => !attended.Contains(q.Name)) + // One node queue per running process, named with a fresh guid each start, so old + // ones pile up by design and are not a signal worth reporting. + .Where(q => !q.Name.StartsWith($"{prefix}.node", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + // Reported per lane, not per queue: a lane is three queues, and listing them separately + // triples a list that is already long enough to bury the ones holding messages. + return orphans + .GroupBy(q => Regex.Replace(q.Name, @"\.(retry|bad)$", "", RegexOptions.IgnoreCase), + StringComparer.OrdinalIgnoreCase) + .Select(lane => new UnattendedQueueView( + lane.Key, + lane.Where(q => !IsRetry(q.Name) && !IsBad(q.Name)).Sum(q => q.Messages), + lane.Where(q => IsRetry(q.Name)).Sum(q => q.Messages), + lane.Where(q => IsBad(q.Name)).Sum(q => q.Messages), + lane.Count())) + .OrderByDescending(l => l.Messages + l.RetryMessages + l.DeadMessages) + .ThenBy(l => l.QueueName) + .ToArray(); + } + + private static bool IsRetry(string name) => name.EndsWith(".retry", StringComparison.OrdinalIgnoreCase); + private static bool IsBad(string name) => name.EndsWith(".bad", StringComparison.OrdinalIgnoreCase); +} diff --git a/SW.Bitween.Api/Resources/Partners/Create.cs b/SW.Bitween.Api/Resources/Partners/Create.cs index 00f3f1d6..ee2de929 100644 --- a/SW.Bitween.Api/Resources/Partners/Create.cs +++ b/SW.Bitween.Api/Resources/Partners/Create.cs @@ -2,7 +2,6 @@ using SW.Bitween.Model; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Partners { @@ -19,9 +18,13 @@ public Create(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(PartnerCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Partners.Create); var entity = new Partner(model.Name); + // Same field the update handler writes, applied in the same transaction as + // the insert, so a partner is never created half-configured. + if (model.AdapterProperties != null) + entity.AdapterProperties = model.AdapterProperties; _dbContext.Add(entity); await _dbContext.SaveChangesAsync(); return entity.Id; diff --git a/SW.Bitween.Api/Resources/Partners/Delete.cs b/SW.Bitween.Api/Resources/Partners/Delete.cs index 16cc960e..1bb7486a 100644 --- a/SW.Bitween.Api/Resources/Partners/Delete.cs +++ b/SW.Bitween.Api/Resources/Partners/Delete.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Partners { @@ -23,7 +22,7 @@ public Delete(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Partners.Delete); if (key == Partner.SystemId) throw new SWException("System partner can not be deleted."); diff --git a/SW.Bitween.Api/Resources/Partners/Get.cs b/SW.Bitween.Api/Resources/Partners/Get.cs index bf96629b..e6cbb299 100644 --- a/SW.Bitween.Api/Resources/Partners/Get.cs +++ b/SW.Bitween.Api/Resources/Partners/Get.cs @@ -11,14 +11,18 @@ namespace SW.Bitween.Resources.Partners public class Get : IGetHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public Get(BitweenDbContext dbContext) + public Get(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } async public Task Handle(int key) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Partners.View); + return await dbContext.Set().AsNoTracking(). Search("Id", key). Select(partner => new PartnerUpdate diff --git a/SW.Bitween.Api/Resources/Partners/Search.cs b/SW.Bitween.Api/Resources/Partners/Search.cs index e43cc81d..60f7a037 100644 --- a/SW.Bitween.Api/Resources/Partners/Search.cs +++ b/SW.Bitween.Api/Resources/Partners/Search.cs @@ -4,28 +4,45 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; +using SW.Bitween.Domain.Gateway; using SW.Bitween.Model; +using System.Collections.Generic; namespace SW.Bitween.Resources.Partners { public class Search : ISearchyHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } async public Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await requestContext.EnsurePermission(dbContext, Model.Permissions.Partners.View); + var query = from subscriber in dbContext.Set() select new PartnerRow { Id = subscriber.Id, Name = subscriber.Name, - SubscriptionsCount = subscriber.Subscriptions.Count, + // Every place the partner is wired in, matching the three groups the + // detail page lists. Counting only the direct subscription link left + // "Used by" reading "—" for partners plainly in use through a gateway. + SubscriptionsCount = subscriber.Subscriptions.Count + + dbContext.Set().Count(g => g.PartnerId == subscriber.Id) + + dbContext.Set().Count(r => r.PartnerId == subscriber.Id), Keys = subscriber.ApiCredentials.Count, + // Selected so the key names can be lifted out below. AdapterProperties + // is a JSON column, so picking its keys in SQL would be provider-specific. + AdapterProperties = subscriber.AdapterProperties, }; query = query.AsNoTracking(); @@ -35,10 +52,19 @@ async public Task Handle(SearchyRequest searchyRequest, bool lookup = fa return await query.Search(searchyRequest.Conditions).ToDictionaryAsync(k => k.Id.ToString(), v => v.Name); } + var result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync(); + + foreach (var row in result) + { + row.PropertyKeys = row.AdapterProperties?.Keys.ToList() ?? new List(); + // Values can be secrets, so the list ships the names and nothing else. + row.AdapterProperties = null; + } + return new SearchyResponse { TotalCount = await query.Search(searchyRequest.Conditions).CountAsync(), - Result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync() + Result = result }; } diff --git a/SW.Bitween.Api/Resources/Partners/Update.cs b/SW.Bitween.Api/Resources/Partners/Update.cs index 52d6601d..e18bfe76 100644 --- a/SW.Bitween.Api/Resources/Partners/Update.cs +++ b/SW.Bitween.Api/Resources/Partners/Update.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Partners { @@ -25,7 +24,7 @@ public Update(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key, PartnerUpdate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Partners.Edit); var entity = await _dbContext.FindAsync(key); entity.SetApiCredentials(model.ApiCredentials.Select(kv => new ApiCredential(kv.Key, kv.Value))); diff --git a/SW.Bitween.Api/Resources/Permissions/Get.cs b/SW.Bitween.Api/Resources/Permissions/Get.cs new file mode 100644 index 00000000..da2652f4 --- /dev/null +++ b/SW.Bitween.Api/Resources/Permissions/Get.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Permissions; + +/// +/// The permission catalog. Static data — the management UI builds its role matrix from this, so +/// the grants it offers can't drift from what the handlers actually enforce. +/// +public class Get : IQueryHandler +{ + public Task Handle() => Task.FromResult(PermissionCatalog.Areas); +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs b/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs new file mode 100644 index 00000000..7d5376d3 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs @@ -0,0 +1,97 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Lists the failures one group caught for one subscription — what a row of +/// spent its budget on. +/// +/// +/// +/// Separate from and asked for one pair at a time, because a policy with fifty +/// subscriptions would otherwise pay for fifty of these joins to answer a question about one row. +/// +/// +/// Only failures carrying a group id appear, so nothing recorded before the group was stamped onto +/// results is listed. A pair whose counter is well spent can therefore come back empty, which is +/// why is the count of what is listable rather than the +/// counter's own value. +/// +/// +[HandlerName("attempts")] +public class Attempts : ICommandHandler +{ + /// + /// Enough to show what keeps failing without turning one table row into a page. The caller is + /// told the total, so a short list never reads as the whole story. + /// + private const int Limit = 10; + + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Attempts(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, RetryGroupAttemptsRequest request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.View); + + // Both halves of the pair have to belong to the policy in the route. For the subscription + // that keeps this from becoming a way to read any subscription's failures through any + // policy id; for the group it is about the answer being readable — an unknown group would + // otherwise report zero failures, which is indistinguishable from a group that genuinely + // has none. + var policy = await _dbContext.Set().AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == key); + if (policy == null) throw new SWNotFoundException(key.ToString()); + + if (policy.Groups.All(g => g.Id != request.GroupId)) + throw new SWNotFoundException($"{key}/{request.GroupId}"); + + var belongs = await _dbContext.Set().AsNoTracking() + .AnyAsync(s => s.Id == request.SubscriptionId && s.RetryPolicyId == key); + if (!belongs) throw new SWNotFoundException($"{key}/{request.SubscriptionId}"); + + var query = from result in _dbContext.Set() + join xchange in _dbContext.Set() on result.Id equals xchange.Id + join pending in _dbContext.Set() on result.Id equals pending.Id into scheduled + from pending in scheduled.DefaultIfEmpty() + where xchange.SubscriptionId == request.SubscriptionId + && result.RetryGroupId == request.GroupId + select new RetryGroupAttemptRow + { + XchangeId = result.Id, + AttemptNumber = result.AttemptNumber, + FailedOn = result.FinishedOn, + Exception = result.Exception, + // A row survives here only until its retry runs, which is what separates a failure + // still being worked on from one that has been given up. + RetryPending = pending != null, + RetryBlockedReason = result.RetryBlockedReason + }; + + query = query.AsNoTracking(); + + return new RetryGroupAttempts + { + Total = await query.CountAsync(), + // Pending first, so the ones still moving cannot be pushed out of the list by a long + // history of failures that are already over. + Attempts = await query + .OrderByDescending(r => r.RetryPending) + .ThenByDescending(r => r.FailedOn) + .Take(Limit) + .ToListAsync() + }; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Create.cs b/SW.Bitween.Api/Resources/RetryPolicies/Create.cs index 4cb049f3..e7ea0d07 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Create.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Create.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -19,12 +18,22 @@ public Create(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(RetryPolicyCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Create); + RetryGroupValidation.EnsureCanFire(model.Groups); + RetryGroupValidation.EnsureAlertTransportIsSecure( + model.AlertHandlerId, model.AlertHandlerProperties); + + // A new policy has nothing stored behind a sentinel, so any that arrives — from a policy + // copied out of Get, say — is dropped rather than saved as the literal password. + foreach (var group in model.Groups ?? []) + AdapterSecretProperties.MergeInPlace(null, group.AlertHandlerProperties); var entity = new RetryPolicy { Name = model.Name, - Groups = model.Groups ?? [] + Groups = model.Groups ?? [], + AlertHandlerId = model.AlertHandlerId, + AlertHandlerProperties = AdapterSecretProperties.Merge(null, model.AlertHandlerProperties) }; _dbContext.Add(entity); await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs b/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs index 571835e4..0afbdc67 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.EfCoreExtensions; using SW.PrimitiveTypes; @@ -21,14 +20,36 @@ public Delete(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Delete); var inUse = await _dbContext.Set() .AnyAsync(s => s.RetryPolicyId == key); if (inUse) throw new SWException("Cannot delete a retry policy that is assigned to one or more subscriptions."); + // Same reason as Update: the policy's groups are about to stop existing, so clear their + // usage rows rather than strand them. + var policy = await _dbContext.FindAsync(key); + var groupIds = policy.Groups.Select(g => g.Id).ToList(); + + // One change, one commit — same reasoning as Update: a half-done delete leaves rows keyed + // by groups that no longer exist anywhere, which nothing can then reach. + await using var transaction = await _dbContext.Database.BeginTransactionAsync(); + await _dbContext.DeleteByKeyAsync(key); + + if (groupIds.Count > 0) + { + await _dbContext.Set() + .Where(u => groupIds.Contains(u.GroupId)) + .ExecuteDeleteAsync(); + + await _dbContext.Set() + .Where(o => groupIds.Contains(o.GroupId)) + .ExecuteDeleteAsync(); + } + + await transaction.CommitAsync(); return null; } } diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Get.cs b/SW.Bitween.Api/Resources/RetryPolicies/Get.cs index 7241dc80..d5c279e6 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Get.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Get.cs @@ -11,22 +11,41 @@ namespace SW.Bitween.Resources.RetryPolicies; public class Get : IGetHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + private readonly AdapterSecretProperties _secrets; - public Get(BitweenDbContext dbContext) + public Get(BitweenDbContext dbContext, RequestContext requestContext, AdapterSecretProperties secrets) { _dbContext = dbContext; + _requestContext = requestContext; + _secrets = secrets; } public async Task Handle(int key) { - return await _dbContext.Set() + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.View); + + // Materialize first: AlertHandlerProperties is a JSON-converted dictionary, and EF cannot + // translate a further .ToDictionary() over it into SQL inside a projection. + var policy = await _dbContext.Set() .AsNoTracking() .Search("Id", key) - .Select(p => new RetryPolicyUpdate - { - Name = p.Name, - Groups = p.Groups - }) .SingleOrDefaultAsync(); + + if (policy == null) return null; + + // Every level that can carry a handler can carry that handler's password, so every level is + // masked. Groups are edited in place by the caller, which is what Update then merges back. + foreach (var group in policy.Groups) + await _secrets.MaskInPlace(group.AlertHandlerId, group.AlertHandlerProperties); + + return new RetryPolicyUpdate + { + Name = policy.Name, + Groups = policy.Groups, + AlertHandlerId = policy.AlertHandlerId, + AlertHandlerProperties = + await _secrets.Mask(policy.AlertHandlerId, policy.AlertHandlerProperties) + }; } } diff --git a/SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs b/SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs new file mode 100644 index 00000000..33286630 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Clears spent group budget, letting an exhausted group retry again. A budget that has run out also +/// clears itself when the integration next succeeds, so this is for putting one back before that +/// happens, or for handing back a total that is spent but not yet exhausted. +/// +[HandlerName("resetusage")] +public class ResetUsage : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public ResetUsage(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, RetryPolicyResetUsage request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Edit); + + var policy = await _dbContext.Set().AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == key); + if (policy == null) throw new SWNotFoundException(key.ToString()); + + // Scope the reset to this policy's own integrations and groups, so a policy id in the + // route can never clear a counter belonging to a different policy. + var subscriptionIds = await _dbContext.Set() + .Where(s => s.RetryPolicyId == key) + .Select(s => s.Id) + .ToListAsync(); + + var groupIds = policy.Groups.Select(g => g.Id).ToList(); + + var query = _dbContext.Set() + .Where(u => subscriptionIds.Contains(u.SubscriptionId) && groupIds.Contains(u.GroupId)); + + if (request.SubscriptionId.HasValue) + query = query.Where(u => u.SubscriptionId == request.SubscriptionId.Value); + + if (request.GroupId.HasValue) + query = query.Where(u => u.GroupId == request.GroupId.Value); + + await query.ExecuteDeleteAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs b/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs new file mode 100644 index 00000000..2586b33b --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SW.Bitween.Model; +using SW.Bitween.NativeAdapters.SmtpHandler; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Rejects retry groups that could never fire. The evaluator skips such groups silently +/// (matchers must support the result type being evaluated — see +/// ), which reads as "retries just don't work", so the +/// misconfiguration is caught at write time instead. +/// +public static class RetryGroupValidation +{ + public static void EnsureCanFire(IEnumerable groups) + { + foreach (var group in groups ?? []) + { + if ((group.AppliesTo?.Count ?? 0) == 0) + throw new SWValidationException("RETRY_GROUP_NO_RESULT_TYPE", + $"Group '{group.Name}' applies to no result type, so it would never be evaluated. " + + "Select Error, Bad result, or both."); + + if ((group.Matchers?.Count ?? 0) == 0) + throw new SWValidationException("RETRY_GROUP_NO_MATCHERS", + $"Group '{group.Name}' has no matchers, so it would never match a failure. " + + "Add at least one matcher."); + + foreach (var resultType in group.AppliesTo) + if (!group.Matchers.Any(m => m.Supports(resultType))) + throw new SWValidationException("RETRY_GROUP_INCOMPATIBLE_MATCHERS", + $"Group '{group.Name}' applies to {resultType} but none of its matchers can be " + + $"evaluated against {resultType} content. {SupportedMatchersFor(resultType)}"); + + // Allow with no budget has nothing to work from — no per-message cap, no total, no delay — + // so the evaluator can only refuse it. Caught here because a group saved that way silently + // stops retrying, which reads as the whole feature being broken. + if (group.Action == RetryAction.Allow && group.Budget == null) + throw new SWValidationException("RETRY_GROUP_ALLOW_WITHOUT_BUDGET", + $"Group '{group.Name}' allows retries but has no budget. Set the attempt caps and " + + "delay, or change the action to block."); + + // An overriding level replaces the one above it rather than merging into it, so a group + // set to Send with no handler would silence the policy's alert instead of redirecting it. + if (group.AlertMode == RetryAlertMode.Send && string.IsNullOrWhiteSpace(group.AlertHandlerId)) + throw new SWValidationException("RETRY_GROUP_ALERT_NO_HANDLER", + $"Group '{group.Name}' is set to send its own budget alert but has no handler. " + + "Choose a handler, or set the alert back to inherit."); + + EnsureAlertTransportIsSecure(group.AlertHandlerId, group.AlertHandlerProperties); + } + } + + /// + /// Rejects an alert override that claims to send but names nothing to send with — the same trap + /// as guards at group level. + /// + public static void EnsureAlertCanSend(RetryAlertMode mode, string handlerId) + { + if (mode == RetryAlertMode.Send && string.IsNullOrWhiteSpace(handlerId)) + throw new SWValidationException("RETRY_ALERT_NO_HANDLER", + "This override is set to send its own budget alert but has no handler. " + + "Choose a handler, or set it back to inherit."); + } + + /// + /// Rejects mail alert settings that would hand the password to an unencrypted connection. + /// + /// + /// + /// The handler refuses this at send time too, which is the guarantee that matters — properties + /// can also arrive straight through the API or out of a global values set. Catching it here is + /// so the person configuring it finds out when they save, rather than from a missing alert and a + /// line in the log days later. + /// + /// + /// Only the mail handler is named, because only it has a password. A general answer belongs in + /// the adapter contract — an adapter saying which of its own settings conflict — not here. + /// + /// + public static void EnsureAlertTransportIsSecure( + string handlerId, IReadOnlyDictionary properties) + { + if (properties == null || properties.Count == 0) return; + if (!nameof(NativeSmtpHandler).Equals(handlerId, StringComparison.OrdinalIgnoreCase)) return; + + // A masked password counts as set: the sentinel means one is stored, not that the field is + // empty. Only an explicit "false" turns encryption off — absent means the adapter's own + // default, which is on. + var password = Value(properties, nameof(SmtpHandlerInput.Password)); + var useTls = Value(properties, nameof(SmtpHandlerInput.UseTls)); + + if (string.IsNullOrWhiteSpace(password)) return; + if (!bool.TryParse(useTls, out var encrypted) || encrypted) return; + + throw new SWValidationException("ALERT_PASSWORD_WITHOUT_TLS", + "This alert would send its mail password over an unencrypted connection. " + + "Turn UseTls on, or clear the password if the relay does not need one."); + } + + private static string Value(IReadOnlyDictionary properties, string key) => + properties.FirstOrDefault(kv => kv.Key.Equals(key, StringComparison.OrdinalIgnoreCase)).Value; + + private static string SupportedMatchersFor(XchangeResultType resultType) => resultType switch + { + XchangeResultType.Error => "Error supports Contains, Regex and Exception type matchers.", + XchangeResultType.BadResult => "Bad result supports Contains, Regex and JSON path matchers.", + _ => "Successful results are never retried." + }; +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs b/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs new file mode 100644 index 00000000..45d2bf4a --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Sets, changes or clears where one subscription's alerts go for one group of this policy — the most +/// specific level of the hierarchy. +/// +[HandlerName("savealertoverride")] +public class SaveAlertOverride : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + private readonly AdapterSecretProperties _secrets; + + public SaveAlertOverride(BitweenDbContext dbContext, RequestContext requestContext, + AdapterSecretProperties secrets) + { + _dbContext = dbContext; + _requestContext = requestContext; + _secrets = secrets; + } + + public async Task Handle(int key, RetryAlertOverrideSave request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Edit); + RetryGroupValidation.EnsureAlertCanSend(request.AlertMode, request.AlertHandlerId); + RetryGroupValidation.EnsureAlertTransportIsSecure( + request.AlertHandlerId, request.AlertHandlerProperties); + + var policy = await _dbContext.Set().AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == key); + if (policy == null) throw new SWNotFoundException(key.ToString()); + + // Scoped to this policy's own groups and subscriptions, so a policy id in the route cannot + // reach an override belonging to a different policy. + if (policy.Groups.All(g => g.Id != request.GroupId)) + throw new SWValidationException("GROUP_NOT_IN_POLICY", + "That group does not belong to this retry policy."); + + var usesPolicy = await _dbContext.Set() + .AnyAsync(s => s.Id == request.SubscriptionId && s.RetryPolicyId == key); + if (!usesPolicy) + throw new SWValidationException("SUBSCRIPTION_NOT_USING_POLICY", + "That subscription does not use this retry policy."); + + var existing = await _dbContext.Set() + .FirstOrDefaultAsync(o => o.SubscriptionId == request.SubscriptionId + && o.GroupId == request.GroupId); + + // Inherit is the absence of an override, so store nothing rather than a row that does nothing + // — otherwise the routing list would have to explain a row that changes no behaviour. + if (request.AlertMode == RetryAlertMode.Inherit) + { + if (existing != null) _dbContext.Remove(existing); + await _dbContext.SaveChangesAsync(); + return null; + } + + // A masked secret has to be restored from whichever level the caller was shown it at. Usage + // masks two things for a pair: the override's own properties, and the properties of the level + // it currently inherits from. Overriding an inherited alert starts from the second — there is + // no override row yet — so both are offered here, with the override's own winning. + var group = policy.Groups.First(g => g.Id == request.GroupId); + var inherited = RetryAlertResolver.Resolve(existing, group, policy); + + var restoreFrom = new Dictionary(); + foreach (var kv in inherited?.HandlerProperties ?? new Dictionary()) + restoreFrom[kv.Key] = kv.Value; + foreach (var kv in existing?.AlertHandlerProperties ?? new Dictionary()) + restoreFrom[kv.Key] = kv.Value; + + var properties = AdapterSecretProperties.Merge(restoreFrom, request.AlertHandlerProperties); + + if (existing == null) + { + _dbContext.Add(new RetryAlertOverride + { + SubscriptionId = request.SubscriptionId, + GroupId = request.GroupId, + AlertMode = request.AlertMode, + AlertHandlerId = request.AlertHandlerId, + AlertHandlerProperties = properties + }); + } + else + { + existing.AlertMode = request.AlertMode; + existing.AlertHandlerId = request.AlertHandlerId; + existing.AlertHandlerProperties = properties; + } + + await _dbContext.SaveChangesAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Search.cs b/SW.Bitween.Api/Resources/RetryPolicies/Search.cs index c41261f6..39ab5e7b 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Search.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Search.cs @@ -11,14 +11,21 @@ namespace SW.Bitween.Resources.RetryPolicies; public class Search : ISearchyHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; } public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.View); + var query = from policy in _dbContext.Set() select new RetryPolicyRow { diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Test.cs b/SW.Bitween.Api/Resources/RetryPolicies/Test.cs index 8fbf12ec..3a89613e 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Test.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Test.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -14,29 +13,33 @@ namespace SW.Bitween.Resources.RetryPolicies; [HandlerName("test")] public class Test : ICommandHandler { + private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; - public Test(RequestContext requestContext) + public Test(BitweenDbContext dbContext, RequestContext requestContext) { + _dbContext = dbContext; _requestContext = requestContext; } - public Task Handle(TestRetryPolicyRequest request) + public async Task Handle(TestRetryPolicyRequest request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + // A pure simulation with no side effects, so viewing a policy is enough to dry-run one. + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.View); if (request.ResultType == XchangeResultType.Success) throw new SWValidationException("INVALID_RESULT_TYPE", "Choose Error or Bad result — a successful result is never retried."); var policy = new CustomRetryPolicy { Groups = request.Groups ?? [] }; - var evaluator = new RetryPolicyEvaluator(policy); + // In-memory budget: a dry-run must not spend any real integration's total. + var evaluator = new RetryPolicyEvaluator(policy, new InMemoryRetryGroupBudget()); var attemptsToSimulate = Math.Clamp(request.AttemptsToSimulate, 1, 20); var attempts = new List(); for (var attemptIndex = 0; attemptIndex < attemptsToSimulate; attemptIndex++) { - var decision = evaluator.Evaluate(request.ResultType, request.Content, attemptIndex); + var decision = await evaluator.Evaluate(request.ResultType, request.Content, attemptIndex); attempts.Add(new TestRetryAttemptResult { @@ -53,6 +56,6 @@ public Task Handle(TestRetryPolicyRequest request) if (!decision.ShouldRetry) break; } - return Task.FromResult(new TestRetryPolicyResponse { Attempts = attempts }); + return new TestRetryPolicyResponse { Attempts = attempts }; } } diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Update.cs b/SW.Bitween.Api/Resources/RetryPolicies/Update.cs index 5d815fd4..05e6b357 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Update.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Update.cs @@ -1,6 +1,7 @@ +using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -19,12 +20,58 @@ public Update(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key, RetryPolicyUpdate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Edit); + RetryGroupValidation.EnsureCanFire(model.Groups); + RetryGroupValidation.EnsureAlertTransportIsSecure( + model.AlertHandlerId, model.AlertHandlerProperties); var entity = await _dbContext.FindAsync(key); + + // Spent budget is keyed by group id, so a group removed here would leave usage rows + // that no policy claims — invisible to the usage report and beyond the reach of reset. + var removedGroupIds = entity.Groups + .Select(g => g.Id) + .Except((model.Groups ?? []).Select(g => g.Id)) + .ToList(); + + // Dropping the groups and clearing what belonged to them is one change, so it commits as + // one: a group no policy claims whose usage and override rows survive is unreachable from + // the usage report and from reset alike. Safe to span, because RetryPolicy raises no domain + // events — nothing reaches the bus before the commit. + await using var transaction = await _dbContext.Database.BeginTransactionAsync(); + + // Secrets came out of Get masked, so put them back from what this same level already holds. + // A group matched by id, because a group added in this very save has nothing to restore from. + foreach (var group in model.Groups ?? []) + { + var storedGroup = entity.Groups.FirstOrDefault(g => g.Id == group.Id); + AdapterSecretProperties.MergeInPlace( + storedGroup?.AlertHandlerProperties, group.AlertHandlerProperties); + } + + var storedPolicyProperties = entity.AlertHandlerProperties; + entity.Name = model.Name; entity.Groups = model.Groups ?? []; + entity.AlertHandlerId = model.AlertHandlerId; + entity.AlertHandlerProperties = + AdapterSecretProperties.Merge(storedPolicyProperties, model.AlertHandlerProperties); await _dbContext.SaveChangesAsync(); + + if (removedGroupIds.Count > 0) + { + await _dbContext.Set() + .Where(u => removedGroupIds.Contains(u.GroupId)) + .ExecuteDeleteAsync(); + + // Alert overrides are keyed by group id for the same reason usage is, so they strand the + // same way when a group disappears. + await _dbContext.Set() + .Where(o => removedGroupIds.Contains(o.GroupId)) + .ExecuteDeleteAsync(); + } + + await transaction.CommitAsync(); return null; } } diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs b/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs new file mode 100644 index 00000000..f72cb526 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Reports the state of every subscription-and-group pair using this policy: how much of the +/// group's total budget that subscription has spent, and where the pair's budget-exhausted alert +/// would go. +/// +/// +/// +/// Both halves share the (SubscriptionId, GroupId) key, so they are reported together — the +/// question worth asking about an exhausted budget is whether anyone was told about it, and +/// splitting that across two reports leaves the caller to join them by eye. +/// +/// +/// Starts from the policy's groups rather than from the stored counters, so a subscription that has +/// never failed still gets a row and its alert override stays configurable before the first failure. +/// Counters for groups no longer in the policy are therefore left out, which is what +/// and delete outright. +/// +/// +[HandlerName("usage")] +public class Usage : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + private readonly RetryUsageReport _report; + + public Usage(BitweenDbContext dbContext, RequestContext requestContext, RetryUsageReport report) + { + _dbContext = dbContext; + _requestContext = requestContext; + _report = report; + } + + public async Task Handle(int key, RetryPolicyUsageRequest request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.View); + + var policy = await _dbContext.Set().AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == key); + if (policy == null) throw new SWNotFoundException(key.ToString()); + + var subscriptions = await _dbContext.Set().AsNoTracking() + .Where(s => s.RetryPolicyId == key) + .Select(s => new { s.Id, s.Name }) + .ToListAsync(); + + return await _report.Build( + subscriptions.Select(s => (s.Id, s.Name)).ToList(), policy.Groups, policy); + } +} diff --git a/SW.Bitween.Api/Resources/Roles/Create.cs b/SW.Bitween.Api/Resources/Roles/Create.cs new file mode 100644 index 00000000..a159ad66 --- /dev/null +++ b/SW.Bitween.Api/Resources/Roles/Create.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using FluentValidation; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Roles; + +public class Create : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Create(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(RoleCreate model) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Roles.Create); + + RoleValidation.EnsureKnownPermissions(model.Permissions); + await RoleValidation.EnsureNameIsFree(_dbContext, model.Name); + + var role = new Role(model.Name, model.Description, model.Permissions); + _dbContext.Add(role); + await _dbContext.SaveChangesAsync(); + return role.Id; + } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.Name).NotEmpty().MaximumLength(100); + RuleFor(i => i.Description).MaximumLength(500); + } + } +} diff --git a/SW.Bitween.Api/Resources/Roles/Delete.cs b/SW.Bitween.Api/Resources/Roles/Delete.cs new file mode 100644 index 00000000..b8520454 --- /dev/null +++ b/SW.Bitween.Api/Resources/Roles/Delete.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Roles; + +public class Delete : IDeleteHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Delete(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Roles.Delete); + + var role = await RoleValidation.Load(_dbContext, key); + + if (role.IsSystem) + throw new SWValidationException("ROLE_IS_BUILT_IN", + $"'{role.Name}' is a built-in role and can't be deleted."); + + var memberCount = await _dbContext.Set().CountAsync(l => l.RoleId == key); + if (memberCount > 0) + throw new SWValidationException("ROLE_IN_USE", + $"'{role.Name}' is still assigned to {memberCount} member{(memberCount == 1 ? "" : "s")}. " + + "Move them to another role first."); + + _dbContext.Remove(role); + await _dbContext.SaveChangesAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/Roles/Get.cs b/SW.Bitween.Api/Resources/Roles/Get.cs new file mode 100644 index 00000000..0f7b4fed --- /dev/null +++ b/SW.Bitween.Api/Resources/Roles/Get.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Roles; + +public class Get : IGetHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Get(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Roles.View); + + var row = await _dbContext.Set() + .AsNoTracking() + .Where(role => role.Id == key) + .Select(role => new RoleRow + { + Id = role.Id, + Name = role.Name, + Description = role.Description, + IsSystem = role.IsSystem, + Permissions = role.Permissions, + CreatedOn = role.CreatedOn, + MemberCount = _dbContext.Set().Count(l => l.RoleId == role.Id) + }) + .SingleOrDefaultAsync(); + + if (row is not null && row.IsSystem) + row.Permissions = Role.SystemPermissions(row.Id); + + return row; + } +} diff --git a/SW.Bitween.Api/Resources/Roles/RoleValidation.cs b/SW.Bitween.Api/Resources/Roles/RoleValidation.cs new file mode 100644 index 00000000..1e872a86 --- /dev/null +++ b/SW.Bitween.Api/Resources/Roles/RoleValidation.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Roles; + +internal static class RoleValidation +{ + /// + /// Rejects keys the catalog doesn't define. The entity also sanitizes, but failing loudly at + /// the boundary surfaces a stale UI instead of silently dropping the grants it asked for. + /// + public static void EnsureKnownPermissions(List permissions) + { + var unknown = (permissions ?? []) + .Where(k => !PermissionCatalog.AllKeys.Contains(k)) + .Distinct() + .ToList(); + + if (unknown.Count > 0) + throw new SWValidationException("UNKNOWN_PERMISSIONS", + $"These permissions don't exist: {string.Join(", ", unknown)}."); + } + + public static async Task EnsureNameIsFree(BitweenDbContext dbContext, string name, int? exceptId = null) + { + var taken = await dbContext.Set() + .AnyAsync(r => r.Name == name && (exceptId == null || r.Id != exceptId)); + + if (taken) + throw new SWValidationException("ROLE_EXISTS", $"A role named '{name}' already exists."); + } + + public static async Task Load(BitweenDbContext dbContext, int key) + { + var role = await dbContext.Set().FindAsync(key); + if (role is null) + throw new SWValidationException("ROLE_NOT_FOUND", $"No role exists with the id {key}."); + return role; + } +} diff --git a/SW.Bitween.Api/Resources/Roles/Search.cs b/SW.Bitween.Api/Resources/Roles/Search.cs new file mode 100644 index 00000000..0ec1f919 --- /dev/null +++ b/SW.Bitween.Api/Resources/Roles/Search.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.EfCoreExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Roles; + +public class Search : ISearchyHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Search(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Roles.View); + + var query = from role in _dbContext.Set() + select new RoleRow + { + Id = role.Id, + Name = role.Name, + Description = role.Description, + IsSystem = role.IsSystem, + Permissions = role.Permissions, + CreatedOn = role.CreatedOn, + MemberCount = _dbContext.Set().Count(l => l.RoleId == role.Id) + }; + + query = query.AsNoTracking(); + + if (lookup) + return await query.Search(searchyRequest.Conditions) + .ToDictionaryAsync(k => k.Id.ToString(), v => v.Name); + + var result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, + searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync(); + + // Built-in roles derive their grants from the catalog, so fill them in after the query. + foreach (var row in result.Where(r => r.IsSystem)) + row.Permissions = Role.SystemPermissions(row.Id); + + return new SearchyResponse + { + TotalCount = await query.Search(searchyRequest.Conditions).CountAsync(), + Result = result + }; + } +} diff --git a/SW.Bitween.Api/Resources/Roles/Update.cs b/SW.Bitween.Api/Resources/Roles/Update.cs new file mode 100644 index 00000000..5667f887 --- /dev/null +++ b/SW.Bitween.Api/Resources/Roles/Update.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using FluentValidation; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Roles; + +public class Update : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Update(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, RoleUpdate model) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Roles.Edit); + + var role = await RoleValidation.Load(_dbContext, key); + + // Built-in roles are the floor an instance can always fall back to — if Administrator + // could be edited, an admin could lock everyone out of members and roles for good. + if (role.IsSystem) + throw new SWValidationException("ROLE_IS_BUILT_IN", + $"'{role.Name}' is a built-in role and can't be changed. Create a role instead."); + + RoleValidation.EnsureKnownPermissions(model.Permissions); + await RoleValidation.EnsureNameIsFree(_dbContext, model.Name, key); + + role.Update(model.Name, model.Description, model.Permissions); + await _dbContext.SaveChangesAsync(); + return null; + } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.Name).NotEmpty().MaximumLength(100); + RuleFor(i => i.Description).MaximumLength(500); + } + } +} diff --git a/SW.Bitween.Api/Resources/Settings/Config.cs b/SW.Bitween.Api/Resources/Settings/Config.cs index c6a95b5d..c8603170 100644 --- a/SW.Bitween.Api/Resources/Settings/Config.cs +++ b/SW.Bitween.Api/Resources/Settings/Config.cs @@ -27,7 +27,10 @@ public async Task Handle() IsRabbitMqManagementConfigured = !string.IsNullOrWhiteSpace(_BitweenOptions.RabbitMqManagementUrl) && !string.IsNullOrWhiteSpace(_BitweenOptions.RabbitMqManagementUsername) && !string.IsNullOrWhiteSpace(_BitweenOptions.RabbitMqManagementPassword), - Theme = _themeOptions + Theme = _themeOptions, + // The product defaults, so the sign-in page — which has no session and can't read the + // settings list — can tell a brand value someone chose from one nobody has touched. + ThemeDefaults = SettingsService.DefaultsUnder("Theme.") }; } } diff --git a/SW.Bitween.Api/Resources/Settings/Delete.cs b/SW.Bitween.Api/Resources/Settings/Delete.cs new file mode 100644 index 00000000..c78f5cde --- /dev/null +++ b/SW.Bitween.Api/Resources/Settings/Delete.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.Bitween.Services; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Settings; + +/// +/// Resets one setting to the product default — the value the options class ships with. +/// +/// The row is rewritten rather than deleted: a missing row is how startup recognises a key it has +/// never imported, so dropping it would let configuration seep back in on the next boot. Reset +/// therefore means "stop choosing", not "forget this key exists". +/// +/// +public class Delete( + BitweenDbContext dbContext, + RequestContext requestContext, + SettingsService settings, + IInfolinkCache cache, + IServiceProvider serviceProvider) : IDeleteHandler +{ + public async Task Handle(string key) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Settings.Edit); + + var definition = SettingsCatalog.Find(key) + ?? throw new SWValidationException("SETTING_NOT_FOUND", $"'{key}' is not a known setting."); + + if (!definition.Stored) + throw new SWValidationException("SETTING_NOT_EDITABLE", + $"{definition.Label} comes from this instance's configuration, so there's nothing to reset."); + + if (!settings.CanStore(definition)) + throw new SWValidationException("SETTING_ENCRYPTION_UNAVAILABLE", + $"{definition.Label} is a secret that isn't stored, so there's nothing to reset."); + + var productDefault = SettingsService.DefaultOf(definition); + var stored = await dbContext.Set().FindAsync(definition.Key); + var toStore = settings.ToStored(definition, productDefault); + + if (stored is null) + dbContext.Add(new Setting { Id = definition.Key, Value = toStore }); + else + stored.Value = toStore; + + await dbContext.SaveChangesAsync(); + + // Resetting an already-default setting is a no-op, not an error — the UI can fire this for + // a staged reset it never saved a value for. + settings.Apply(definition, productDefault); + + // A reset has to take effect the same way a write does — see Update. + if (definition.OnChange is not null) await definition.OnChange(serviceProvider); + + await cache.BroadcastRevoke(); + + return null; + } +} diff --git a/SW.Bitween.Api/Resources/Settings/Get.cs b/SW.Bitween.Api/Resources/Settings/Get.cs new file mode 100644 index 00000000..84614464 --- /dev/null +++ b/SW.Bitween.Api/Resources/Settings/Get.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.Bitween.Services; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Settings; + +/// +/// Every editable setting, in catalog order: the definition from +/// joined with the stored value. Rows normally exist for all of them — startup imports whatever +/// configuration had — so a missing row means the key couldn't be stored yet. +/// +public class Get(BitweenDbContext dbContext, RequestContext requestContext, SettingsService settings) + : IQueryHandler +{ + public async Task Handle() + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Settings.View); + + var rows = await dbContext.Set().AsNoTracking() + .ToDictionaryAsync(s => s.Id, s => s.Value, StringComparer.OrdinalIgnoreCase); + + return SettingsCatalog.All.Select(definition => + { + // Environment-owned settings have no row to join to: they're reported straight from + // the options object, either with their value or as set/not set. + if (!definition.Stored) return EnvironmentRow(definition, settings.LiveValue(definition)); + + var hasRow = rows.TryGetValue(definition.Key, out var stored); + var productDefault = SettingsService.DefaultOf(definition); + // A secret's value is withheld either way round: only whether one is set is public. + var value = definition.Secret ? null : hasRow ? stored : productDefault; + // A secret has no product default, and its ciphertext couldn't be compared with one + // anyway — so for a secret both "is set" and "is overridden" mean the same thing: + // a non-empty value is stored. + var secretIsSet = hasRow && !string.IsNullOrEmpty(stored); + + return new SettingRow + { + Key = definition.Key, + Section = definition.Section, + Label = definition.Label, + Description = definition.Description, + Kind = definition.Kind.ToString().ToLowerInvariant(), + DefaultValue = definition.Secret ? string.Empty : productDefault, + Value = value, + Secret = definition.Secret, + Overridden = definition.Secret ? secretIsSet : hasRow && stored != productDefault, + HasValue = definition.Secret + ? secretIsSet + : !string.IsNullOrEmpty(hasRow ? stored : productDefault), + // The only thing that makes a stored setting uneditable: a secret with no + // passphrase configured to protect it. + Editable = settings.CanStore(definition), + Access = Name(definition.Access) + }; + }).ToArray(); + } + + /// + /// A setting the UI reports but can't change. There's no default and nothing to reset, so + /// those stay empty; a presence setting withholds the value the same way a secret does. + /// + private static SettingRow EnvironmentRow(SettingDefinition definition, string live) => new() + { + Key = definition.Key, + Section = definition.Section, + Label = definition.Label, + Description = definition.Description, + Kind = definition.Kind.ToString().ToLowerInvariant(), + DefaultValue = string.Empty, + Value = definition.Access == SettingAccess.Presence ? null : live, + Secret = false, + Overridden = false, + HasValue = !string.IsNullOrEmpty(live), + Editable = false, + Access = Name(definition.Access) + }; + + private static string Name(SettingAccess access) => access.ToString().ToLowerInvariant(); +} diff --git a/SW.Bitween.Api/Resources/Settings/Update.cs b/SW.Bitween.Api/Resources/Settings/Update.cs new file mode 100644 index 00000000..f47a2f6f --- /dev/null +++ b/SW.Bitween.Api/Resources/Settings/Update.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.Bitween.Services; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Settings; + +/// +/// Stores a new value for one setting. It's applied to the live options singletons as well as +/// stored, so the very next request already sees it; the cache-revoke broadcast carries the change +/// to any other instance. Secrets are encrypted before they're written. Only editable settings can +/// be written at all — the environment-owned ones the page also lists are rejected here. +/// +public class Update( + BitweenDbContext dbContext, + RequestContext requestContext, + SettingsService settings, + IInfolinkCache cache, + IServiceProvider serviceProvider) : ICommandHandler +{ + public async Task Handle(string key, SettingUpdate request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Settings.Edit); + + var definition = SettingsCatalog.Find(key) + ?? throw new SWValidationException("SETTING_NOT_FOUND", $"'{key}' is not a known setting."); + + if (!definition.Stored) + throw new SWValidationException("SETTING_NOT_EDITABLE", + $"{definition.Label} comes from this instance's configuration and can't be changed here."); + + if (!settings.CanStore(definition)) + throw new SWValidationException("SETTING_ENCRYPTION_UNAVAILABLE", + $"{definition.Label} is a secret and can only be stored once " + + $"{BitweenOptions.ConfigurationSection}:{nameof(BitweenOptions.SettingsEncryptionKey)} is configured."); + + // Empty is a real value — it's how you clear an optional link or a license key. + var value = request?.Value ?? string.Empty; + + try + { + SettingsService.Validate(definition, value); + } + catch (FormatException ex) + { + throw new SWValidationException("SETTING_INVALID_VALUE", $"{definition.Label}: {ex.Message}"); + } + + await Store(definition, value); + settings.Apply(definition, value); + + // Some settings need more than the property assignment above — re-scheduling a job, for + // instance. Runs after Apply so the hook reads the value that's now in effect. + if (definition.OnChange is not null) await definition.OnChange(serviceProvider); + + await cache.BroadcastRevoke(); + + return null; + } + + /// + /// Writes the value, creating the row if startup hasn't imported this key yet. Every setting + /// keeps exactly one row, keyed by the catalog key. + /// + private async Task Store(SettingDefinition definition, string value) + { + var stored = await dbContext.Set().FindAsync(definition.Key); + var toStore = settings.ToStored(definition, value); + + if (stored is null) + dbContext.Add(new Setting { Id = definition.Key, Value = toStore }); + else + stored.Value = toStore; + + await dbContext.SaveChangesAsync(); + } +} diff --git a/SW.Bitween.Api/Resources/SubscriptionCategories/Search.cs b/SW.Bitween.Api/Resources/SubscriptionCategories/Search.cs index d532712e..f6e1ccd1 100644 --- a/SW.Bitween.Api/Resources/SubscriptionCategories/Search.cs +++ b/SW.Bitween.Api/Resources/SubscriptionCategories/Search.cs @@ -16,10 +16,13 @@ public Search(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; _requestContext = requestContext; + _requestContext = requestContext; } public async Task Handle(SearchSubscriptionCategoryModel request) { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.View); + request.Limit ??= 20; request.Offset ??= 0; var q = _dbContext.Set().AsNoTracking().AsQueryable(); diff --git a/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs b/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs index 653583c6..00c4cdbb 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs @@ -2,7 +2,6 @@ using SW.Bitween.Model; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Subscriptions { @@ -22,7 +21,7 @@ public AggregateNow(BitweenDbContext dbContext, RequestContext requestContext, S public async Task Handle(int key, SubscriptionAggregateNow request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Operate); var entity = await _dbContext.FindAsync(key); entity.SetAggregateNow(); diff --git a/SW.Bitween.Api/Resources/Subscriptions/Create.cs b/SW.Bitween.Api/Resources/Subscriptions/Create.cs index 2597bfbf..f9f74ccb 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Create.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Create.cs @@ -1,26 +1,46 @@ -using FluentValidation; +using FluentValidation; using SW.Bitween.Domain; using SW.Bitween.Model; using SW.PrimitiveTypes; +using System; +using System.Collections.Generic; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Subscriptions { - public class Create : ICommandHandler + /// + /// Creates a subscription, complete, in one transaction. + /// + /// It used to accept only a name, a document and a type, so a client wanting a working + /// integration had to POST and then PATCH. The POST committed on its own, so a rejected + /// PATCH left an empty subscription behind that nobody asked for and nothing cleaned up. + /// Everything now lands in a single SaveChangesAsync: either the integration exists + /// as asked for, or it does not exist. + /// + /// + /// The pipeline is optional. A caller sending only the original four fields still gets the + /// empty, inactive subscription it always did. + /// + /// + public class Create : ICommandHandler { private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; + private readonly IInfolinkCache _BitweenCache; + private readonly SubscriptionSchedulerService _subScheduler; - public Create(BitweenDbContext dbContext, RequestContext requestContext) + public Create(BitweenDbContext dbContext, RequestContext requestContext, + IInfolinkCache BitweenCache, SubscriptionSchedulerService subScheduler) { this._dbContext = dbContext; _requestContext = requestContext; + _BitweenCache = BitweenCache; + _subScheduler = subScheduler; } public async Task Handle(SubscriptionCreate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Create); Subscription entity; @@ -40,23 +60,53 @@ public async Task Handle(SubscriptionCreate model) case SubscriptionType.BusGateway: entity = new Subscription(model.Name, model.DocumentId, model.Type); break; - + case SubscriptionType.Unknown: default: throw new BitweenException(); } var trail = new SubscriptionTrail(SubscriptionTrialCode.Created, entity, true); - _dbContext.Add(trail); + + // Same code the update handler applies, so a field can't work on one and not the other. + await SubscriptionConfigurationApplier.Apply(_dbContext, entity, model); + + // Every constructor starts it inactive. Only an explicit false turns that around, so a + // caller that doesn't mention it keeps the behaviour it has always had. + if (model.Inactive.HasValue) entity.Inactive = model.Inactive.Value; + await _dbContext.SaveChangesAsync(); + + // Both of these used to be the follow-up update's job. Now that a subscription can be + // born live and scheduled, skipping them would leave a new integration that looks + // configured and never runs: stale in the consumers' cache, absent from the scheduler. + await _BitweenCache.BroadcastRevoke(); + await _subScheduler.Sync(entity, Array.Empty()); + return entity.Id; } private class Validate : AbstractValidator { - public Validate() + public Validate(BitweenDbContext dbContext, AdapterRequirements adapterRequirements) { + // Same rule Documents enforces on BusMessageTypeName. Publishing to a name no + // information type carries is legitimate — the consumer may be another product — + // but it still becomes a RabbitMQ routing key, and a name with a space in it + // could never be answered by an information type anyway. + RuleFor(i => i.ResponseMessageTypeName) + .Matches("^\\S+$") + .When(i => !string.IsNullOrEmpty(i.ResponseMessageTypeName)) + .WithMessage("A bus message name cannot contain spaces."); + + RuleFor(i => i.ResponseSubscriptionId).CustomAsync(async (responseSubId, context, _) => + { + var failure = await ResponseRoutingValidation.CheckDestination(dbContext, responseSubId); + if (failure != null) + context.AddFailure(nameof(SubscriptionCreate.ResponseSubscriptionId), failure); + }); + RuleFor(i => i.Name).NotEmpty(); RuleFor(i => i.DocumentId).NotEmpty().When(i => i.Type != SubscriptionType.Aggregation); RuleFor(i => i.PartnerId).NotEqual(Partner.SystemId); @@ -74,7 +124,42 @@ public Validate() When(i => i.Type == SubscriptionType.Aggregation, () => { RuleFor(i => i.AggregationForId).NotEmpty(); }); + + // An adapter that is named has to be usable. Nothing is required to be named — + // create without a pipeline is still legitimate — but a half-configured adapter + // is the failure this endpoint exists to stop committing. + RuleFor(i => i.ReceiverProperties).CustomAsync(async (provided, context, _) => + await AddMissing(context, adapterRequirements, + ((SubscriptionCreate)context.InstanceToValidate).ReceiverId, provided)); + + RuleFor(i => i.ValidatorProperties).CustomAsync(async (provided, context, _) => + await AddMissing(context, adapterRequirements, + ((SubscriptionCreate)context.InstanceToValidate).ValidatorId, provided)); + + RuleFor(i => i.MapperProperties).CustomAsync(async (provided, context, _) => + await AddMissing(context, adapterRequirements, + ((SubscriptionCreate)context.InstanceToValidate).MapperId, provided)); + + RuleFor(i => i.HandlerProperties).CustomAsync(async (provided, context, _) => + await AddMissing(context, adapterRequirements, + ((SubscriptionCreate)context.InstanceToValidate).HandlerId, provided)); + + // Schedules only mean anything on the two scheduled types, and an empty set is + // what Subscription.SetSchedules rejects outright. + RuleFor(i => i.Schedules) + .NotEmpty() + .When(i => i.Schedules != null && + (i.Type == SubscriptionType.Receiving || i.Type == SubscriptionType.Aggregation)) + .WithMessage("Schedules cannot be empty for a scheduled subscription."); + } + + private static async Task AddMissing(ValidationContext context, + AdapterRequirements adapterRequirements, string adapterId, ICollection provided) + { + var missing = await adapterRequirements.MissingFor(adapterId, provided); + if (missing.Count > 0) + context.AddFailure($"Missing: {string.Join(",", missing)}"); } } } -} \ No newline at end of file +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/Delete.cs b/SW.Bitween.Api/Resources/Subscriptions/Delete.cs index dbb1c597..2fe856e4 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Delete.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Delete.cs @@ -1,11 +1,11 @@ -using SW.EfCoreExtensions; +using Microsoft.EntityFrameworkCore; +using SW.EfCoreExtensions; using SW.Bitween.Domain; +using SW.Bitween.Domain.Gateway; using SW.PrimitiveTypes; -using System; using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Subscriptions { @@ -23,10 +23,69 @@ public Delete(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Viewer); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Delete); + + await EnsureNothingPointsAtIt(key); await _dbContext.DeleteByKeyAsync(key); return null; } + + /// + /// Says what still points at the integration, before the database says it less politely. + /// + /// + /// All four references are RESTRICT, so the delete was already refused — but as a + /// foreign key violation surfacing as a 500, which tells the operator nothing about which + /// route or gateway is holding it, and reads like a broken screen rather than a decision. + /// Exchanges are deliberately not checked: their reference is nullable and history is not + /// a reason to keep configuration alive. + /// + private async Task EnsureNothingPointsAtIt(int key) + { + var heldBy = new List(); + + var routeGateways = await _dbContext.Set() + .Where(r => r.SubscriptionId == key) + .Select(r => r.BusGateway.Name) + .Distinct() + .ToArrayAsync(); + if (routeGateways.Length > 0) + heldBy.Add($"a route on {Join(routeGateways)}"); + + var attachmentGateways = await _dbContext.Set() + .Where(p => p.SubscriptionId == key) + .Select(p => p.ApiGateway.Name) + .Distinct() + .ToArrayAsync(); + if (attachmentGateways.Length > 0) + heldBy.Add($"a partner attached to {Join(attachmentGateways)}"); + + var fedBy = await _dbContext.Set() + .Where(s => s.ResponseSubscriptionId == key) + .Select(s => s.Name) + .ToArrayAsync(); + if (fedBy.Length > 0) + heldBy.Add($"the response of {Join(fedBy)}"); + + var aggregatedBy = await _dbContext.Set() + .Where(s => s.AggregationForId == key) + .Select(s => s.Name) + .ToArrayAsync(); + if (aggregatedBy.Length > 0) + heldBy.Add($"the aggregation {Join(aggregatedBy)}"); + + if (heldBy.Count == 0) + return; + + throw new SWValidationException("SUBSCRIPTION_IN_USE", + $"This integration is still used by {Join(heldBy.ToArray())}. " + + "Remove that first, or point it at another integration."); + } + + private static string Join(string[] names) => + names.Length == 1 + ? names[0] + : $"{string.Join(", ", names[..^1])} and {names[^1]}"; } -} \ No newline at end of file +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/Get.cs b/SW.Bitween.Api/Resources/Subscriptions/Get.cs index 120855e4..71de4794 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Get.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Get.cs @@ -14,20 +14,24 @@ namespace SW.Bitween.Resources.Subscriptions public class Get : IGetHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; private readonly NativeAdapterDiscoveryService _nativeAdapterDiscovery; private readonly IServiceProvider _serviceProvider; private const string PrivateSentinel = "__private__"; - public Get(BitweenDbContext dbContext, NativeAdapterDiscoveryService nativeAdapterDiscovery, IServiceProvider serviceProvider) + public Get(BitweenDbContext dbContext, NativeAdapterDiscoveryService nativeAdapterDiscovery, IServiceProvider serviceProvider, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; _nativeAdapterDiscovery = nativeAdapterDiscovery; _serviceProvider = serviceProvider; } public async Task Handle(int key) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.View); + var subscriber = await dbContext.Set().AsNoTracking().Search("Id", key).SingleOrDefaultAsync(); @@ -50,6 +54,7 @@ public async Task Handle(int key) Type = subscriber.Type, Temporary = subscriber.Temporary, ResponseSubscriptionId = subscriber.ResponseSubscriptionId, + ResponseMessageTypeName = subscriber.ResponseMessageTypeName, ReceiveOn = subscriber.ReceiveOn, AggregateOn = subscriber.AggregateOn, ConsecutiveFailures = subscriber.ConsecutiveFailures, diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetLastRuns.cs b/SW.Bitween.Api/Resources/Subscriptions/GetLastRuns.cs new file mode 100644 index 00000000..0136ef28 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/GetLastRuns.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using SW.Scheduler; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// The newest run of every scheduled subscription, so a list can show a last-run +/// column without asking per row. +/// +[HandlerName("lastruns")] +public class GetLastRuns : IQueryHandler +{ + /// How many recent runs the success ratio is measured over. + private const int RecentWindow = 20; + + private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; + private readonly IScheduleRepository scheduleRepo; + private readonly SchedulerOptions schedulerOptions; + + public GetLastRuns( + BitweenDbContext dbContext, + RequestContext requestContext, + IScheduleRepository scheduleRepo, + SchedulerOptions schedulerOptions) + { + this.dbContext = dbContext; + this.requestContext = requestContext; + this.scheduleRepo = scheduleRepo; + this.schedulerOptions = schedulerOptions; + } + + public async Task Handle(SearchSubscriptionLastRunsModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.View); + + var subscriptions = await dbContext.Set() + .AsNoTracking() + .Where(s => s.Type == SubscriptionType.Receiving || s.Type == SubscriptionType.Aggregation) + .Select(s => new { s.Id, s.Type }) + .ToListAsync(); + + var since = DateTime.UtcNow.AddDays(-schedulerOptions.RetentionDays); + var results = new List(); + + // One small indexed query per scheduled subscription. The subscription id + // only exists inside the execution's JSON parameter, so there is nothing to + // GROUP BY — but each of these is an ordered top-N, and the alternative + // (pulling a whole retention window of every job's executions and grouping + // in memory) is far worse on an instance with chatty schedules. + // + // Taking the window rather than just the newest row gets the success ratio + // out of the same query instead of a second aggregate per subscription. + foreach (var s in subscriptions) + { + var job = scheduleRepo.GetJobDefinitions() + .Single(d => d.JobType == SubscriptionRunHistory.JobTypeFor(s.Type)); + var manualPrefix = SubscriptionRunHistory.ManualPrefix(job); + + var window = await SubscriptionRunHistory + .Query(dbContext, job, s.Id, since) + .Take(RecentWindow) + .Select(j => new + { + j.StartTimeUtc, + j.EndTimeUtc, + j.DurationMs, + j.Success, + j.Error, + j.Node, + Manual = j.JobName.StartsWith(manualPrefix) + }) + .ToListAsync(); + + if (window.Count == 0) continue; + + var last = window[0]; + results.Add(new SubscriptionLastRunModel + { + SubscriptionId = s.Id, + StartedOn = last.StartTimeUtc, + EndedOn = last.EndTimeUtc, + DurationMs = last.DurationMs, + Success = last.Success, + Error = last.Error, + Node = last.Node, + Manual = last.Manual, + RecentTotal = window.Count(j => j.Success != null), + RecentSucceeded = window.Count(j => j.Success == true) + }); + } + + return results; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs b/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs new file mode 100644 index 00000000..c162e898 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Paged, filterable history of one Receiving subscription's own +/// rows — independent of , which reads the scheduler's own (always-succeeds) +/// history instead. +/// +[HandlerName("receiveattempts")] +public class GetReceiveAttempts : IQueryHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public GetReceiveAttempts(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(SearchReceiveAttemptsModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.View); + + var offset = request.Offset ?? 0; + var limit = request.Limit ?? 25; + + var query = _dbContext.Set() + .AsNoTracking() + .Where(a => a.SubscriptionId == request.SubscriptionId); + + if (request.Outcome.HasValue) + query = query.Where(a => a.Outcome == request.Outcome.Value); + + var totalCount = await query.CountAsync(); + + var page = await query + .OrderByDescending(a => a.StartedOn) + .Skip(offset) + .Take(limit) + .ToListAsync(); + + var exchangeIds = page.SelectMany(a => a.ExchangeIds ?? Array.Empty()).Distinct().ToList(); + + // Left join: an id an attempt still points at but whose Xchange got cleaned up some + // other way shows up with nulls rather than silently dropping the row's own history. + var exchangesById = await ( + from x in _dbContext.Set() + join r in _dbContext.Set() on x.Id equals r.Id into xr + from r in xr.DefaultIfEmpty() + join p in _dbContext.Set() on x.Id equals p.Id into xp + from p in xp.DefaultIfEmpty() + where exchangeIds.Contains(x.Id) + select new ReceiveAttemptExchangeRef + { + Id = x.Id, + Status = r.Success, + ResponseBad = r.ResponseBad, + PromotedProperties = p == null ? null : p.Properties.ToDictionary(), + } + ).ToDictionaryAsync(e => e.Id); + + var result = page.Select(a => new ReceiveAttemptModel + { + Id = a.Id, + StartedOn = a.StartedOn, + FinishedOn = a.FinishedOn, + Outcome = a.Outcome, + ErrorMessage = a.ErrorMessage, + Exchanges = (a.ExchangeIds ?? Array.Empty()) + .Select(id => exchangesById.TryGetValue(id, out var x) ? x : new ReceiveAttemptExchangeRef { Id = id }) + .ToList(), + }).ToList(); + + return new + { + Result = result, + TotalCount = totalCount, + }; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetRuns.cs b/SW.Bitween.Api/Resources/Subscriptions/GetRuns.cs new file mode 100644 index 00000000..76ba2024 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/GetRuns.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using SW.Scheduler; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Execution history for one scheduled subscription, read from the scheduler's own +/// job_executions table (populated by AddSchedulerMonitoring, bounded by +/// ). +/// +[HandlerName("runs")] +public class GetRuns : IQueryHandler +{ + private const int MaxLimit = 100; + + private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; + private readonly IScheduleRepository scheduleRepo; + private readonly SchedulerOptions schedulerOptions; + + public GetRuns( + BitweenDbContext dbContext, + RequestContext requestContext, + IScheduleRepository scheduleRepo, + SchedulerOptions schedulerOptions) + { + this.dbContext = dbContext; + this.requestContext = requestContext; + this.scheduleRepo = scheduleRepo; + this.schedulerOptions = schedulerOptions; + } + + public async Task Handle(SearchSubscriptionRunsModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.View); + + var limit = Math.Clamp(request.Limit ?? 20, 1, MaxLimit); + + var type = await dbContext.Set() + .AsNoTracking() + .Where(s => s.Id == request.SubscriptionId) + .Select(s => (SubscriptionType?)s.Type) + .SingleOrDefaultAsync(); + + if (type == null) + throw new SWNotFoundException($"Subscription {request.SubscriptionId} was not found."); + + var jobType = SubscriptionRunHistory.JobTypeFor(type.Value); + if (jobType == null) + return new List(); + + var job = scheduleRepo.GetJobDefinitions().Single(d => d.JobType == jobType); + var manualPrefix = SubscriptionRunHistory.ManualPrefix(job); + var since = DateTime.UtcNow.AddDays(-schedulerOptions.RetentionDays); + + return await SubscriptionRunHistory + .Query(dbContext, job, request.SubscriptionId, since) + .Take(limit) + .Select(j => new SubscriptionRunModel + { + StartedOn = j.StartTimeUtc, + EndedOn = j.EndTimeUtc, + DurationMs = j.DurationMs, + Success = j.Success, + Error = j.Error, + Node = j.Node, + Manual = j.JobName.StartsWith(manualPrefix) + }) + .ToListAsync(); + } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.SubscriptionId).NotEmpty(); + } + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetScheduleHealth.cs b/SW.Bitween.Api/Resources/Subscriptions/GetScheduleHealth.cs new file mode 100644 index 00000000..e166972b --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/GetScheduleHealth.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Quartz; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using SW.Scheduler; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Answers "will this schedule actually fire?" by asking the scheduler rather than +/// trusting what Bitween stored. Covers the two ways a job goes quiet without any +/// error surfacing: a trigger that is missing or not in a firing state, and a +/// subscription left flagged as running so its concurrency guard blocks every fire. +/// +[HandlerName("schedulehealth")] +public class GetScheduleHealth : IQueryHandler +{ + /// + /// Mirrors SW.Scheduler's internal Constants.JobParamsKey — the Quartz data-map + /// entry it serializes the job parameter under. Stable: it is persisted in + /// qrtz_job_details, so changing it would break existing schedules. + /// + private const string JobParamsKey = "JobParams"; + + private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; + private readonly IScheduleRepository scheduleRepo; + private readonly ISchedulerFactory schedulerFactory; + + public GetScheduleHealth( + BitweenDbContext dbContext, + RequestContext requestContext, + IScheduleRepository scheduleRepo, + ISchedulerFactory schedulerFactory) + { + this.dbContext = dbContext; + this.requestContext = requestContext; + this.scheduleRepo = scheduleRepo; + this.schedulerFactory = schedulerFactory; + } + + public async Task Handle(SearchSubscriptionScheduleHealthModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.View); + + // Inactive subscriptions are unscheduled on purpose, so they have no triggers + // and reporting them as broken would be noise. + var subscriptions = await dbContext.Set() + .AsNoTracking() + .Where(s => + (s.Type == SubscriptionType.Receiving || s.Type == SubscriptionType.Aggregation) && + !s.Inactive) + .ToListAsync(); + + if (subscriptions.Count == 0) + return new List(); + + var scheduler = await schedulerFactory.GetScheduler(); + var jobs = scheduleRepo.GetJobDefinitions().ToDictionary(d => d.JobType); + var busy = await CurrentlyRunningSubscriptionIds(scheduler, jobs); + + var results = new List(); + + foreach (var sub in subscriptions) + { + var job = jobs[SubscriptionRunHistory.JobTypeFor(sub.Type)]; + var prefix = sub.Type == SubscriptionType.Receiving ? "receiver" : "aggregator"; + + var states = new List(); + DateTime? next = null; + + foreach (var schedule in sub.Schedules) + { + var jobKey = new JobKey(ScheduleToCronExtension.ScheduleKeyFor(prefix, sub.Id, schedule), job.Group); + + foreach (var trigger in await scheduler.GetTriggersOfJob(jobKey)) + { + states.Add(await scheduler.GetTriggerState(trigger.Key)); + + var fire = trigger.GetNextFireTimeUtc()?.UtcDateTime; + if (fire != null && (next == null || fire < next)) + next = fire; + } + } + + results.Add(new SubscriptionScheduleHealthModel + { + SubscriptionId = sub.Id, + ScheduleCount = sub.Schedules.Count, + TriggerCount = states.Count, + State = WorstState(states, sub.Schedules.Count), + NextFireOn = next, + Stuck = sub.IsRunning && !busy.Contains(sub.Id) + }); + } + + return results; + } + + /// + /// Subscription ids the scheduler is executing right now. This — not the + /// job-execution rows — is what distinguishes a genuinely running job from a + /// stale IsRunning flag: a killed run never writes its end either, so its + /// execution row stays open forever and would look identical. + /// + private static async Task> CurrentlyRunningSubscriptionIds( + IScheduler scheduler, Dictionary jobs) + { + var groups = jobs.Values.Select(d => d.Group).ToHashSet(); + var running = new HashSet(); + + foreach (var context in await scheduler.GetCurrentlyExecutingJobs()) + { + if (!groups.Contains(context.JobDetail.Key.Group)) continue; + + var json = context.JobDetail.JobDataMap.GetString(JobParamsKey); + if (string.IsNullOrEmpty(json)) continue; + + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("subscriptionId", out var id) && id.TryGetInt32(out var value)) + running.Add(value); + } + + return running; + } + + /// The state worth reporting: anything that stops a fire outranks the ones that don't. + private static string WorstState(IReadOnlyCollection states, int scheduleCount) + { + // No trigger where a schedule says there should be one — the job simply + // isn't registered with the scheduler and nothing will ever fire it. + if (states.Count < scheduleCount) return "Missing"; + if (states.Count == 0) return "Missing"; + + if (states.Contains(TriggerState.Error)) return "Error"; + if (states.Contains(TriggerState.Blocked)) return "Blocked"; + if (states.Contains(TriggerState.Paused)) return "Paused"; + if (states.Contains(TriggerState.None)) return "Missing"; + if (states.All(s => s == TriggerState.Complete)) return "Complete"; + return "Normal"; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetTrail.cs b/SW.Bitween.Api/Resources/Subscriptions/GetTrail.cs index 9894fd06..b9e6079c 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/GetTrail.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/GetTrail.cs @@ -14,15 +14,19 @@ namespace SW.Bitween.Resources.Subscriptions; public class GetTrail : IQueryHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public GetTrail(BitweenDbContext dbContext) + public GetTrail(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } public async Task Handle(SearchSubscriptionTrailModel request) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.View); + request.Limit ??= 20; request.Offset ??= 0; var trails = dbContext.Set() diff --git a/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs b/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs new file mode 100644 index 00000000..212c86eb --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs @@ -0,0 +1,123 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using System.Text.RegularExpressions; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions +{ + /// + /// Turns an integration defined inline — while a gateway route or attachment is being made — + /// into a subscription, ready to be saved alongside whatever points at it. + /// + /// Nothing here duplicates : the pipeline is applied by the very same + /// , and the caller does the single + /// SaveChangesAsync, so the integration and its link either both exist or neither does. + /// + /// + public static class InlineIntegration + { + /// + /// Which integration a gateway link points at. Exactly one of an existing id and an + /// inline definition has to be given — both, or neither, is a mistake worth naming. + /// + public static void EnsureExactlyOne(int? subscriptionId, InlineIntegrationCreate inline) + { + if (subscriptionId.HasValue && inline != null) + throw new SWValidationException(GatewayLinkTarget.BothGiven, + "Give either an existing integration or a new one to create, not both."); + + if (!subscriptionId.HasValue && inline == null) + throw new SWValidationException(GatewayLinkTarget.NeitherGiven, + "Pick the integration this runs, or define a new one."); + } + + + /// + /// The rules an ordinary create enforces through FluentValidation, applied to an integration + /// arriving this way. Without them this door was a hole in the same validation: a response + /// message name with a space in it went straight to a RabbitMQ routing key nothing can + /// answer. Each check calls the one implementation the create handler calls. + /// + private static async Task CheckConfiguration( + BitweenDbContext dbContext, + AdapterRequirements adapterRequirements, + InlineIntegrationCreate model) + { + if (!string.IsNullOrEmpty(model.ResponseMessageTypeName) + && Regex.IsMatch(model.ResponseMessageTypeName, @"\s")) + throw new SWValidationException("INVALID_BUS_TYPE_NAME", + "A bus message name cannot contain spaces."); + + var responseFailure = await ResponseRoutingValidation.CheckDestination( + dbContext, model.ResponseSubscriptionId); + if (responseFailure != null) + throw new SWValidationException( + ResponseRoutingValidation.BusGatewayCode, responseFailure); + + // Neither gateway type carries its own partner — a partner reaches them through the + // attachment or the route, which is the very thing being made. + if (model.PartnerId.HasValue) + throw new SWValidationException("PARTNER_NOT_ALLOWED", + "A gateway integration does not carry its own partner."); + + // A named adapter has to be usable. Naming none is still fine; a half-configured one + // is exactly what committing here would make permanent. + foreach (var (kind, adapterId, provided) in new[] + { + ("receiver", model.ReceiverId, model.ReceiverProperties), + ("validator", model.ValidatorId, model.ValidatorProperties), + ("mapper", model.MapperId, model.MapperProperties), + ("handler", model.HandlerId, model.HandlerProperties), + }) + { + var missing = await adapterRequirements.MissingFor(adapterId, provided); + if (missing.Count > 0) + throw new SWValidationException("ADAPTER_INCOMPLETE", + $"The {kind} is missing {string.Join(", ", missing)}."); + } + } + + /// + /// Builds the subscription and adds it to the change tracker, so its id is available to + /// the link being created in the same transaction. Does not save. + /// + public static async Task Stage( + BitweenDbContext dbContext, + AdapterRequirements adapterRequirements, + InlineIntegrationCreate model, + int documentId, + SubscriptionType type) + { + if (string.IsNullOrWhiteSpace(model.Name)) + throw new SWValidationException("INVALID_NAME", "Give the integration a name."); + + await CheckConfiguration(dbContext, adapterRequirements, model); + + // Who chooses the information type differs by gateway kind, so the caller passes it: + // a bus gateway is bound to one and imposes it, an API gateway is not and the caller + // picks. Either way it is settled before Apply runs. + if (!await dbContext.Set().AnyAsync(d => d.Id == documentId)) + throw new SWValidationException("INVALID_DOCUMENT", + "Choose the information type this integration carries."); + + model.DocumentId = documentId; + + var entity = new Subscription(model.Name, documentId, type); + var trail = new SubscriptionTrail(SubscriptionTrialCode.Created, entity, true); + dbContext.Add(trail); + + // The same code an ordinary create runs, so a field cannot work through one door + // and not the other. + await SubscriptionConfigurationApplier.Apply(dbContext, entity, model); + + // Neither gateway type runs on its own — a GatewayApiCall waits for an attachment, a + // BusGateway for a route — and the one being made is in this same transaction. + entity.Inactive = false; + + dbContext.Add(entity); + return entity; + } + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/Pause.cs b/SW.Bitween.Api/Resources/Subscriptions/Pause.cs index 44d14d67..c56317ff 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Pause.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Pause.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Newtonsoft.Json; using SW.Bitween.Domain; -using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -23,7 +22,7 @@ public Pause(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key, SubscriptionPause request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Operate); var entity = await _dbContext.FindAsync(key); SubscriptionTrail trail; diff --git a/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs b/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs index 1dbcf31b..b6141b9f 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs @@ -2,7 +2,6 @@ using SW.Bitween.Model; using SW.PrimitiveTypes; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Subscriptions { @@ -22,7 +21,7 @@ public ReceiveNow(BitweenDbContext dbContext, RequestContext requestContext, Sub async public Task Handle(int key, SubscriptionReceiveNow request) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Operate); var entity = await _dbContext.FindAsync(key); entity.SetReceiveNow(); diff --git a/SW.Bitween.Api/Resources/Subscriptions/ResetRetryUsage.cs b/SW.Bitween.Api/Resources/Subscriptions/ResetRetryUsage.cs new file mode 100644 index 00000000..122e19b1 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/ResetRetryUsage.cs @@ -0,0 +1,49 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Clears one subscription's spent retry budget so its groups start retrying again. +/// +/// +/// The policy-scoped reset finds subscriptions by policy id, which leaves an inline +/// CustomRetryPolicy unreachable: its counters are written like any other and then nothing can +/// clear them, so once exhausted that subscription would never retry again. This resets by +/// subscription instead, which also picks up counters left behind by groups that no longer exist. +/// +[HandlerName("resetretryusage")] +public class ResetRetryUsage : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public ResetRetryUsage(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, SubscriptionRetryResetUsage request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Operate); + + if (!await _dbContext.Set().AnyAsync(s => s.Id == key)) + throw new SWNotFoundException(key.ToString()); + + // Scoped by subscription rather than by policy, so it cannot reach anyone else's counters no + // matter which kind of policy this subscription uses. + var query = _dbContext.Set().Where(u => u.SubscriptionId == key); + + if (request.GroupId.HasValue) + query = query.Where(u => u.GroupId == request.GroupId.Value); + + await query.ExecuteDeleteAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs b/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs new file mode 100644 index 00000000..82b4e9d6 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Guards what a delivery response may be fed into. +/// +/// A bus gateway's route is defined by the message that runs it, but +/// creates the response exchange against the chosen +/// subscription directly. Picking a route as the response destination therefore runs that one +/// route with the bus skipped entirely: no message published, no route matching, no filter, and +/// none of the other routes bound to the same message. It looks like publishing and is not, so +/// it is refused rather than left as a trap. ResponseMessageTypeName is the field that +/// actually puts a response on the bus. +/// +/// +internal static class ResponseRoutingValidation +{ + public const string BusGatewayCode = "RESPONSE_SUBSCRIPTION_IS_BUS_GATEWAY"; + + /// Returns the failure message, or null when the destination is allowed. + public static async Task CheckDestination(BitweenDbContext dbContext, int? responseSubscriptionId) + { + if (responseSubscriptionId is null) return null; + + var type = await dbContext.Set().AsNoTracking() + .Where(s => s.Id == responseSubscriptionId.Value) + .Select(s => (SubscriptionType?)s.Type) + .SingleOrDefaultAsync(); + + if (type != SubscriptionType.BusGateway) return null; + + return "A bus gateway route cannot receive a response: it would run with the bus skipped, " + + "so no other route bound to the same message would see it. Publish the response on " + + "the bus instead, and let the gateway's routes pick it up."; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs b/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs new file mode 100644 index 00000000..af5ad0e1 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Reports one subscription's spent retry budget and where each group's exhaustion alert would go. +/// +/// +/// The policy-scoped report answers the same question for every subscription sharing a policy, but it +/// can only find subscriptions by policy id — so a subscription carrying an inline +/// CustomRetryPolicy is invisible to it while still spending and recording budget. Asking from +/// the subscription's side reaches those too. +/// +[HandlerName("retryusage")] +public class RetryUsage : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + private readonly RetryUsageReport _report; + + public RetryUsage(BitweenDbContext dbContext, RequestContext requestContext, RetryUsageReport report) + { + _dbContext = dbContext; + _requestContext = requestContext; + _report = report; + } + + public async Task Handle(int key, RetryPolicyUsageRequest request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.View); + + var subscription = await _dbContext.Set().AsNoTracking() + .Include(s => s.RetryPolicy) + .FirstOrDefaultAsync(s => s.Id == key); + if (subscription == null) throw new SWNotFoundException(key.ToString()); + + // Whichever policy actually applies. An inline one has no row, so the policy level of the + // alert hierarchy simply is not there for it — passed as null, which the resolver expects. + var groups = subscription.CustomRetryPolicy?.Groups ?? subscription.RetryPolicy?.Groups ?? []; + + return await _report.Build( + [(subscription.Id, subscription.Name)], groups, subscription.RetryPolicy); + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/SaveMapper.cs b/SW.Bitween.Api/Resources/Subscriptions/SaveMapper.cs index bacc50e6..7819dd5a 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/SaveMapper.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/SaveMapper.cs @@ -7,7 +7,6 @@ using System; using System.Linq; using System.Threading.Tasks; -using SW.Bitween.Domain.Accounts; namespace SW.Bitween.Resources.Subscriptions { @@ -27,7 +26,7 @@ public SaveMapper(BitweenDbContext dbContext, IInfolinkCache BitweenCache, Reque public async Task Handle(int key, SubscriptionSaveMapper model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Edit); var entity = await _dbContext.FindAsync(key); entity.MapperId = model.MapperId; diff --git a/SW.Bitween.Api/Resources/Subscriptions/Search.cs b/SW.Bitween.Api/Resources/Subscriptions/Search.cs index 4b6c9295..0083bdfb 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Search.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Search.cs @@ -15,11 +15,13 @@ namespace SW.Bitween.Resources.Subscriptions public class Search : ISearchyHandler { private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; private readonly List _edgeCaseProperties; - public Search(BitweenDbContext dbContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext) { _dbContext = dbContext; + _requestContext = requestContext; _edgeCaseProperties = new List { @@ -32,6 +34,11 @@ public Search(BitweenDbContext dbContext) public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.View); + var query = from subscriber in _dbContext.Set() join document in _dbContext.Set() on subscriber.DocumentId equals document.Id select new SubscriptionSearch @@ -51,6 +58,9 @@ join document in _dbContext.Set() on subscriber.DocumentId equals docu ReceiveOn = subscriber.ReceiveOn, PausedOn = subscriber.PausedOn, IsRunning = subscriber.IsRunning, + ConsecutiveFailures = subscriber.ConsecutiveFailures, + LastException = subscriber.LastException, + RetryPolicyId = subscriber.RetryPolicyId, MapperProperties = subscriber.MapperProperties.ToKeyAndValueCollection(), HandlerProperties = subscriber.HandlerProperties.ToKeyAndValueCollection(), ReceiverProperties = subscriber.ReceiverProperties.ToKeyAndValueCollection(), @@ -62,9 +72,9 @@ join document in _dbContext.Set() on subscriber.DocumentId equals docu WorkGroupId = subscriber.WorkGroupId, CategoryDescription = subscriber.Category.Description, CategoryCode = subscriber.Category.Code, - RetryPolicyId = subscriber.RetryPolicyId, CustomRetryPolicy = subscriber.CustomRetryPolicy, - + ResponseSubscriptionId = subscriber.ResponseSubscriptionId, + ResponseMessageTypeName = subscriber.ResponseMessageTypeName, }; query = query.AsNoTracking().AsQueryable(); diff --git a/SW.Bitween.Api/Resources/Subscriptions/SubscriptionConfigurationApplier.cs b/SW.Bitween.Api/Resources/Subscriptions/SubscriptionConfigurationApplier.cs new file mode 100644 index 00000000..f9264d4e --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/SubscriptionConfigurationApplier.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Puts a onto a subscription. Create and Update both +/// go through here, so a field cannot be honoured by one and silently ignored by the other — +/// which is what happened for as long as create couldn't accept a pipeline at all. +/// +/// Deliberately does not touch name, document, partner or aggregation-for. On create those are +/// decided by the constructor, and an Aggregation subscription's DocumentId is +/// rather than anything the caller sent — copying +/// the model over it would corrupt the subscription. +/// +/// +internal static class SubscriptionConfigurationApplier +{ + /// + /// A property whose value the client is not allowed to see. It gets this sentinel instead, + /// and sending it back means "keep what is stored" rather than "set it to this string". + /// + private const string PrivateSentinel = "__private__"; + + public static async Task Apply(BitweenDbContext dbContext, Subscription entity, SubscriptionConfiguration model) + { + entity.ReceiverId = model.ReceiverId; + entity.ValidatorId = model.ValidatorId; + entity.MapperId = model.MapperId; + entity.HandlerId = model.HandlerId; + entity.CategoryId = model.CategoryId; + entity.WorkGroupId = model.WorkGroupId; + entity.ResponseSubscriptionId = model.ResponseSubscriptionId; + entity.ResponseMessageTypeName = model.ResponseMessageTypeName; + + // Only when the caller said something about them. A Receiving subscription with no + // schedules is what SetSchedules throws on, and a create that mentions no schedule at + // all is the old, still-supported "empty subscription" call. + if (model.Schedules != null) + entity.SetSchedules(model.Schedules.Select(dto => new Schedule(dto.Recurrence, + System.TimeSpan.Parse($"{dto.Days}.{dto.Hours}:{dto.Minutes}:0"), dto.Backwards)).ToList()); + + entity.SetDictionaries( + MergeWithOriginal(entity.HandlerProperties, model.HandlerProperties), + MergeWithOriginal(entity.MapperProperties, model.MapperProperties), + MergeWithOriginal(entity.ReceiverProperties, model.ReceiverProperties), + model.DocumentFilter?.ToDictionary() ?? new Dictionary(), + MergeWithOriginal(entity.ValidatorProperties, model.ValidatorProperties) + ); + entity.SetMatchExpression(model.MatchExpression); + + if (model.CustomRetryPolicy == null && model.RetryPolicyId != null && + !await dbContext.Set().AnyAsync(p => p.Id == model.RetryPolicyId)) + throw new SWValidationException("RETRY_POLICY_NOT_FOUND", + $"Retry policy {model.RetryPolicyId} was not found."); + + entity.SetRetryPolicy(model.RetryPolicyId, model.CustomRetryPolicy); + } + + private static Dictionary MergeWithOriginal( + IReadOnlyDictionary original, + ICollection incoming) + { + var result = new Dictionary(); + foreach (var kv in incoming ?? Enumerable.Empty()) + { + if (kv.Value == PrivateSentinel) + { + // Private prop: restore the original stored value, don't overwrite with sentinel. + if (original != null && original.TryGetValue(kv.Key, out var stored)) + result[kv.Key] = stored; + } + else + { + result[kv.Key] = kv.Value; + } + } + return result; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/SubscriptionRunHistory.cs b/SW.Bitween.Api/Resources/Subscriptions/SubscriptionRunHistory.cs new file mode 100644 index 00000000..4618cfb7 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/SubscriptionRunHistory.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.Scheduler; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Shared lookup for reading a subscription's runs out of the scheduler's +/// job_executions table. Used by both (one +/// subscription, full history) and (every scheduled +/// subscription, newest run only). +/// +internal static class SubscriptionRunHistory +{ + /// The job that runs this subscription, or null if it isn't scheduled at all. + public static Type JobTypeFor(SubscriptionType type) => type switch + { + SubscriptionType.Receiving => typeof(ReceivingJob), + SubscriptionType.Aggregation => typeof(AggregationJob), + _ => null + }; + + /// Executions of belonging to one subscription, newest first. + public static IQueryable Query( + BitweenDbContext dbContext, IScheduledJobDefinition job, int subscriptionId, DateTime since) + { + // Executions are keyed per schedule entry, and a manual run gets its own + // generated key entirely — so neither the subscription id nor the set of + // current schedule keys can be matched on JobName. The subscription id is + // in the serialized job parameter instead, which covers all of them: + // scheduled runs, manual runs, and runs of schedules since edited away. + // + // The trailing comma keeps 128 from matching 1289 — safe because + // ReceivingJobParams/AggregationJobParams both carry CronExpression after + // SubscriptionId, so it is never the last property. + var needle = $"\"subscriptionId\":{subscriptionId},"; + + return dbContext.Set() + .AsNoTracking() + .Where(j => j.JobGroup == job.Group && j.StartTimeUtc >= since && j.Context.Contains(needle)) + .OrderByDescending(j => j.StartTimeUtc); + } + + /// A run started by Receive now / Aggregate now rather than by the cron. + public static string ManualPrefix(IScheduledJobDefinition job) => $"{job.Name}_OneTime_"; +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/Update.cs b/SW.Bitween.Api/Resources/Subscriptions/Update.cs index b93101b1..209478e7 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Update.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Update.cs @@ -1,16 +1,14 @@ using FluentValidation; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using SW.EfCoreExtensions; using SW.Bitween.Domain; using SW.Bitween.Model; using SW.PrimitiveTypes; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using SW.Bitween.Domain.Accounts; +using SW.Bitween.Resources.RetryPolicies; namespace SW.Bitween.Resources.Subscriptions { @@ -21,8 +19,6 @@ public class Update : ICommandHandler private readonly RequestContext _requestContext; private readonly SubscriptionSchedulerService _subScheduler; - private const string PrivateSentinel = "__private__"; - public Update(BitweenDbContext dbContext, IInfolinkCache BitweenCache, RequestContext requestContext, SubscriptionSchedulerService subScheduler) { this._dbContext = dbContext; @@ -33,32 +29,27 @@ public Update(BitweenDbContext dbContext, IInfolinkCache BitweenCache, RequestCo public async Task Handle(int key, SubscriptionUpdate model) { - _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Edit); var entity = await _dbContext.FindAsync(key); // Capture before SetSchedules replaces the collection. var oldSchedules = entity.Schedules.ToList(); var trail = new SubscriptionTrail(SubscriptionTrialCode.Updated, entity); - _dbContext.Entry(entity).SetProperties(model); - entity.SetSchedules(model.Schedules.Select(dto => new Schedule(dto.Recurrence, - TimeSpan.Parse($"{dto.Days}.{dto.Hours}:{dto.Minutes}:0"), dto.Backwards)).ToList()); - entity.SetDictionaries( - MergeWithOriginal(entity.HandlerProperties, model.HandlerProperties), - MergeWithOriginal(entity.MapperProperties, model.MapperProperties), - MergeWithOriginal(entity.ReceiverProperties, model.ReceiverProperties), - model.DocumentFilter.ToDictionary(), - MergeWithOriginal(entity.ValidatorProperties, model.ValidatorProperties) - ); - entity.SetMatchExpression(model.MatchExpression); + // Name and the runtime-state fields, which only an update may set. + _dbContext.Entry(entity).SetProperties(model); if (model.CustomRetryPolicy == null && model.RetryPolicyId != null && !await _dbContext.Set().AnyAsync(p => p.Id == model.RetryPolicyId)) throw new SWValidationException("RETRY_POLICY_NOT_FOUND", $"Retry policy {model.RetryPolicyId} was not found."); - entity.SetRetryPolicy(model.RetryPolicyId, model.CustomRetryPolicy); + if (model.CustomRetryPolicy != null) + RetryGroupValidation.EnsureCanFire(model.CustomRetryPolicy.Groups); + + // Everything a person configures, through the same code the create handler runs. + await SubscriptionConfigurationApplier.Apply(_dbContext, entity, model); trail.SetAfter(entity); _dbContext.Add(trail); @@ -71,27 +62,6 @@ public async Task Handle(int key, SubscriptionUpdate model) return null; } - private static System.Collections.Generic.Dictionary MergeWithOriginal( - IReadOnlyDictionary original, - ICollection incoming) - { - var result = new System.Collections.Generic.Dictionary(); - foreach (var kv in incoming ?? Enumerable.Empty()) - { - if (kv.Value == PrivateSentinel) - { - // Private prop: restore the original stored value, don't overwrite with sentinel - if (original != null && original.TryGetValue(kv.Key, out var stored)) - result[kv.Key] = stored; - } - else - { - result[kv.Key] = kv.Value; - } - } - return result; - } - // private static Dictionary ReplaceHiddenData(IReadOnlyDictionary original, // Dictionary updated) // { @@ -145,65 +115,35 @@ private ValueTask GetSub(BitweenDbContext dbContext, IHttpContextA return dbContext.FindAsync(subId); } - public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAccessor, NativeAdapterDiscoveryService nativeAdapterDiscovery, IServiceProvider serviceProvider) + public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAccessor, AdapterRequirements adapterRequirements) { RuleFor(i => i.Name).NotEmpty(); RuleFor(i => i.MatchExpression).Must(ValidateMatch); RuleFor(i => i.PartnerId).NotEqual(Partner.SystemId); + // Matches the rule Documents enforces on BusMessageTypeName; see Create. + RuleFor(i => i.ResponseMessageTypeName) + .Matches("^\\S+$") + .When(i => !string.IsNullOrEmpty(i.ResponseMessageTypeName)) + .WithMessage("A bus message name cannot contain spaces."); When(i => i.MapperId != null, () => { - RuleFor(i => i.MapperProperties).CustomAsync(async (i, context, _) => + RuleFor(i => i.MapperProperties).CustomAsync(async (provided, context, _) => { - var mapperId = ((SubscriptionUpdate)context.InstanceToValidate).MapperId; - var mustProps = Enumerable.Empty(); - - // Check if it's a native adapter - if (mapperId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) - { - var properties = nativeAdapterDiscovery.GetStartupValues(mapperId); - mustProps = properties.Where(p => !p.Value.Optional).Select(p => p.Key); - } - else - { - var serverless = serviceProvider.GetRequiredService(); - await serverless.StartAsync(mapperId, null); - mustProps = (await serverless.GetExpectedStartupValues()) - .Where(p => p.Value.Optional == false).Select(p => p.Key); - } - - var missing = mustProps.ToHashSet(StringComparer.OrdinalIgnoreCase) - .Except(i.Where(p => !string.IsNullOrEmpty(p.Value)).Select(p => p.Key)); - if (missing.Any()) + var missing = await adapterRequirements.MissingFor( + ((SubscriptionUpdate)context.InstanceToValidate).MapperId, provided); + if (missing.Count > 0) context.AddFailure($"Missing: {string.Join(",", missing)}"); }); }); When(i => i.HandlerId != null, () => { - - RuleFor(i => i.HandlerProperties).CustomAsync(async (i, context, ct) => + RuleFor(i => i.HandlerProperties).CustomAsync(async (provided, context, _) => { - var handlerId = ((SubscriptionUpdate)context.InstanceToValidate).HandlerId; - var mustProps = Enumerable.Empty(); - - // Check if it's a native adapter - if (handlerId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) - { - var properties = nativeAdapterDiscovery.GetStartupValues(handlerId); - mustProps = properties.Where(p => !p.Value.Optional).Select(p => p.Key); - } - else - { - var serverless = serviceProvider.GetRequiredService(); - await serverless.StartAsync(handlerId, null); - mustProps = (await serverless.GetExpectedStartupValues()) - .Where(p => p.Value.Optional == false).Select(p => p.Key); - } - - var missing = mustProps.ToHashSet(StringComparer.OrdinalIgnoreCase) - .Except(i.Where(p => !string.IsNullOrEmpty(p.Value)).Select(p => p.Key)); - if (missing.Any()) + var missing = await adapterRequirements.MissingFor( + ((SubscriptionUpdate)context.InstanceToValidate).HandlerId, provided); + if (missing.Count > 0) context.AddFailure($"Missing: {string.Join(",", missing)}"); }); @@ -216,6 +156,15 @@ public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAcce }); }); + // Outside the handler check above: a response destination that can never work is + // wrong whether or not this same request also sets a handler. + RuleFor(i => i.ResponseSubscriptionId).CustomAsync(async (responseSubId, context, ct) => + { + var failure = await ResponseRoutingValidation.CheckDestination(dbContext, responseSubId); + if (failure != null) + context.AddFailure(nameof(SubscriptionUpdate.ResponseSubscriptionId), failure); + }); + RuleFor(i => i).CustomAsync(async (model, context, ct) => { var subscription = await GetSub(dbContext, httpContextAccessor); @@ -228,29 +177,9 @@ public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAcce if (model.Schedules == null || !model.Schedules.Any()) context.AddFailure(nameof(model.Schedules), "Schedules are required for Receiving subscriptions"); - if (!string.IsNullOrEmpty(model.ReceiverId)) - { - var mustProps = Enumerable.Empty(); - - // Check if it's a native adapter - if (model.ReceiverId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) - { - var properties = nativeAdapterDiscovery.GetStartupValues(model.ReceiverId); - mustProps = properties.Where(p => !p.Value.Optional).Select(p => p.Key); - } - else - { - var serverless = serviceProvider.GetRequiredService(); - await serverless.StartAsync(model.ReceiverId, null); - mustProps = (await serverless.GetExpectedStartupValues()) - .Where(p => p.Value.Optional == false).Select(p => p.Key); - } - - var missing = mustProps.ToHashSet(StringComparer.OrdinalIgnoreCase) - .Except(model.ReceiverProperties.Where(p => !string.IsNullOrEmpty(p.Value)).Select(p => p.Key)); - if (missing.Any()) - context.AddFailure(nameof(model.ReceiverProperties), $"Missing properties: {string.Join(",", missing)}"); - } + var missing = await adapterRequirements.MissingFor(model.ReceiverId, model.ReceiverProperties); + if (missing.Count > 0) + context.AddFailure(nameof(model.ReceiverProperties), $"Missing properties: {string.Join(",", missing)}"); } }); diff --git a/SW.Bitween.Api/Resources/WorkGroups/Create.cs b/SW.Bitween.Api/Resources/WorkGroups/Create.cs index f22e0e3a..931f2699 100644 --- a/SW.Bitween.Api/Resources/WorkGroups/Create.cs +++ b/SW.Bitween.Api/Resources/WorkGroups/Create.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; +using FluentValidation; using SW.Bitween.Domain; -using SW.Bitween.Model; +using SW.Bitween.Model; using SW.PrimitiveTypes; namespace SW.Bitween.Resources.WorkGroups; @@ -12,6 +13,8 @@ public class Create(BitweenDbContext dbContext, RequestContext requestContext,II public async Task Handle(CreateWorkGroupModel request) { + await _requestContext.EnsurePermission(dbContext, Model.Permissions.WorkGroups.Create); + var workgroup = new WorkGroup() { Name = request.Name, @@ -34,4 +37,15 @@ public async Task Handle(CreateWorkGroupModel request) workgroup.Id }; } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.BusMessageName) + .Matches("^\\S+$") + .When(i => !string.IsNullOrEmpty(i.BusMessageName)) + .WithMessage("Bus message name cannot contain spaces."); + } + } } \ No newline at end of file diff --git a/SW.Bitween.Api/Resources/WorkGroups/Delete.cs b/SW.Bitween.Api/Resources/WorkGroups/Delete.cs index 05b5dfed..23db025a 100644 --- a/SW.Bitween.Api/Resources/WorkGroups/Delete.cs +++ b/SW.Bitween.Api/Resources/WorkGroups/Delete.cs @@ -14,6 +14,8 @@ public class Delete(BitweenDbContext dbContext, RequestContext requestContext, I public async Task Handle(int key, DeleteWorkGroupModel _) { + await _requestContext.EnsurePermission(dbContext, Model.Permissions.WorkGroups.Delete); + var category = await dbContext.Set().FindAsync(key); if (category is null) throw new SWValidationException("CATEGORY_NOT_FOUND", $"Workgroup with id {key} was not found"); diff --git a/SW.Bitween.Api/Resources/WorkGroups/Search.cs b/SW.Bitween.Api/Resources/WorkGroups/Search.cs index 099b9c7a..d5c85e7d 100644 --- a/SW.Bitween.Api/Resources/WorkGroups/Search.cs +++ b/SW.Bitween.Api/Resources/WorkGroups/Search.cs @@ -11,7 +11,8 @@ namespace SW.Bitween.Resources.WorkGroups; public class Search( - IInfolinkCache infolinkCache, + BitweenDbContext dbContext, + RequestContext requestContext, IConsumerReader consumerReader, ILogger logger) : IQueryHandler @@ -19,10 +20,21 @@ public class Search( public async Task Handle(SearchWorkGroupModel request) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.WorkGroups.View); + request.Limit ??= 20; request.Offset ??= 0; - - var workGroups = await infolinkCache.ListWorkGroupsAsync(); + + // Read straight from the DB rather than IInfolinkCache: that cache only + // invalidates via a broadcast back to itself over RabbitMQ (see + // WorkGroups/Create|Update|Delete.cs calling BroadcastRevoke()), so a + // freshly created/edited/deleted work group can stay invisible here + // until the cache's own TTL expires whenever that broadcast doesn't + // land. GlobalAdapterValuesSets and RetryPolicies already read the DB + // directly for the same reason. + var workGroups = await dbContext.Set().AsNoTracking() + .Where(w => request.Name == null || w.Name.Contains(request.Name)) + .ToArrayAsync(); var consumerCounts = Array.Empty(); try @@ -65,6 +77,7 @@ public async Task Handle(SearchWorkGroupModel request) NotifierIncomingRate = notifiersCounts?.IncomingRate, NotifierProcessingCount = notifiersCounts?.ProcessingCount, NotifierQueueCount = notifiersCounts?.QueueCount, + ProcessorNodeCount = processorsCounts?.TotalNodes, }; }).ToList(); diff --git a/SW.Bitween.Api/Resources/WorkGroups/Update.cs b/SW.Bitween.Api/Resources/WorkGroups/Update.cs index e24ee6b0..7cb8f5d8 100644 --- a/SW.Bitween.Api/Resources/WorkGroups/Update.cs +++ b/SW.Bitween.Api/Resources/WorkGroups/Update.cs @@ -5,14 +5,23 @@ namespace SW.Bitween.Resources.WorkGroups; -public class Update(BitweenDbContext dbContext,IInfolinkCache _BitweenCache, IBroadcast _broadcast) : ICommandHandler +public class Update(BitweenDbContext dbContext, RequestContext requestContext, IInfolinkCache _BitweenCache, IBroadcast _broadcast) : ICommandHandler { + private readonly RequestContext _requestContext = requestContext; + public async Task Handle(int key, CreateWorkGroupModel request) { + await _requestContext.EnsurePermission(dbContext, Model.Permissions.WorkGroups.Edit); + var workGroup = await dbContext.Set().FindAsync(key); if (workGroup is null) throw new SWValidationException("WORK_GROUP_NOT_FOUND", $"Category with id {key} was not found"); + + // Update binds the same CreateWorkGroupModel type as Create, so Create's + // Validate (IValidator) already runs for this request too — + // CqApiController resolves validators by the request's concrete type. workGroup.Name = request.Name; + workGroup.BusMessageName = request.BusMessageName; workGroup.Options = new WorkGroupOptions { RabbitMqOptions = new ConsumerSettings diff --git a/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs b/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs index 282549e5..49065a29 100644 --- a/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs +++ b/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs @@ -44,12 +44,18 @@ public async Task Handle(XchangeBulkRetry request) if (subscription == null) throw new SWValidationException("SUBSCRIPTION_NOT_FOUND", "Cant reset properties, subscription doesnt exist anymore"); - await _xchangeService.CreateXchange(subscription, xchange, xchangeFile); + await _xchangeService.CreateXchange(subscription, xchange, xchangeFile, + manualRetry: true); } else { - await _xchangeService.CreateXchange(xchange, xchangeFile, subscription.WorkGroup); + // Null when the subscription has since been deleted, which a document-only + // exchange also has from the start. The single-exchange retry has always allowed + // for it; without the same here, one such id in a selection threw and took the + // whole bulk retry down with it. + await _xchangeService.CreateXchange(xchange, xchangeFile, subscription?.WorkGroup, + manualRetry: true); } } diff --git a/SW.Bitween.Api/Resources/Xchanges/Create.cs b/SW.Bitween.Api/Resources/Xchanges/Create.cs index e54a9121..55208ede 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Create.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Create.cs @@ -22,7 +22,7 @@ public Create(XchangeService xchangeService, BitweenDbContext dbc) public async Task Handle(CreateXchange request) { - var xchangeFile = new XchangeFile(request.Data); + var xchangeFile = new XchangeFile(request.Data, "manual.json"); if (request.Option == CreateXchangeOption.DocumentId) { var document = await _dbc.Set().FirstOrDefaultAsync(d => d.Id == request.DocumentId); diff --git a/SW.Bitween.Api/Resources/Xchanges/GetInternal.cs b/SW.Bitween.Api/Resources/Xchanges/GetInternal.cs index edadf257..974dc7de 100644 --- a/SW.Bitween.Api/Resources/Xchanges/GetInternal.cs +++ b/SW.Bitween.Api/Resources/Xchanges/GetInternal.cs @@ -15,14 +15,18 @@ namespace SW.Bitween.Resources.Xchanges public class GetInternal : IGetHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; - public GetInternal(BitweenDbContext dbContext) + public GetInternal(BitweenDbContext dbContext, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; } async public Task Handle(int key) { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Exchanges.View); + return await dbContext.Set().AsNoTracking(). Search("Id", key). Select( xchange => new XchangeRow diff --git a/SW.Bitween.Api/Resources/Xchanges/Retry.cs b/SW.Bitween.Api/Resources/Xchanges/Retry.cs index 6b2c034b..3c76cd8f 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Retry.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Retry.cs @@ -34,11 +34,12 @@ public async Task Handle(string key, XchangeRetry xchangeRetry) if (subscription == null) throw new SWValidationException("SUBSCRIPTION_NOT_FOUND", "Cant reset properties, subscription doesnt exist anymore"); - await xchangeService.CreateXchange(subscription, xchange, xchangeFile); + await xchangeService.CreateXchange(subscription, xchange, xchangeFile, manualRetry: true); } else { - await xchangeService.CreateXchange(xchange,xchangeFile,subscription?.WorkGroup ); + await xchangeService.CreateXchange(xchange, xchangeFile, subscription?.WorkGroup, + manualRetry: true); } diff --git a/SW.Bitween.Api/Resources/Xchanges/Search.cs b/SW.Bitween.Api/Resources/Xchanges/Search.cs index aef8e649..cadf7daf 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Search.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Search.cs @@ -15,16 +15,23 @@ namespace SW.Bitween.Resources.Xchanges public class Search : ISearchyHandler { private readonly BitweenDbContext dbContext; + private readonly RequestContext requestContext; private readonly XchangeService xchangeService; - public Search(BitweenDbContext dbContext, XchangeService xchangeService) + public Search(BitweenDbContext dbContext, XchangeService xchangeService, RequestContext requestContext) { this.dbContext = dbContext; + this.requestContext = requestContext; this.xchangeService = xchangeService; } public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) { + // Lookup returns only id/name pairs, which pickers across the app rely on; + // the full list is the data, so that's what the view permission covers. + if (!lookup) + await requestContext.EnsurePermission(dbContext, Model.Permissions.Exchanges.View, Model.Permissions.Dashboard.View); + searchyRequest.DatesToUtc(); await using var dr = await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted); @@ -72,8 +79,14 @@ from delayedRetry in drGroup.DefaultIfEmpty() OutputFileName = result.OutputName, ResponseFileName = result.ResponseName, CorrelationId = xchange.CorrelationId, - PartnerId = subscriber.PartnerId, - ScheduledRetryOn = delayedRetry != null ? delayedRetry.On : (DateTime?)null + // xchange.PartnerId is the authoritative source (set at creation from the + // gateway/bus-route partner, or the subscription's own PartnerId as a + // fallback there too) but the column was added later with no backfill, so + // pre-migration xchanges have it null even when their subscription carries + // a direct PartnerId — fall back to that for those legacy rows. + PartnerId = xchange.PartnerId ?? subscriber.PartnerId, + ScheduledRetryOn = delayedRetry != null ? delayedRetry.On : (DateTime?)null, + RetryBlockedReason = result.RetryBlockedReason }; var condition = searchyRequest.Conditions.FirstOrDefault(); @@ -138,7 +151,11 @@ from delayedRetry in drGroup.DefaultIfEmpty() { var value = propertyFilter.Value.ToString()!.ToLower(); - query = query.Where(i => i.PromotedPropertiesRaw.Contains(value)); + // Both sides lower-cased at query time. Promoted values keep the case the + // payload had (see FilterService), so the column has to be folded here for + // the search to stay case-insensitive. No index is lost: a Contains is a + // leading-wildcard LIKE, which the b-tree on this column could never serve. + query = query.Where(i => i.PromotedPropertiesRaw.ToLower().Contains(value)); condition.Filters.Remove(propertyFilter); } } diff --git a/SW.Bitween.Api/Resources/Xchanges/Update.cs b/SW.Bitween.Api/Resources/Xchanges/Update.cs index bb395ec1..b8191cdc 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Update.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Update.cs @@ -85,7 +85,9 @@ public async Task Handle(string documentIdOrName, dynamic request) var xchangeFile = new XchangeFile(request.ToString()); - await _xchangeService.RunValidator(sub.ValidatorId, sub.ValidatorProperties.ToDictionary(), xchangeFile); + var globalAdapterValuesSets = await _cache.ListGlobalAdapterValuesSetsAsync(); + var validatorProperties = sub.ValidatorProperties.ToDictionary().Fill(par.Partner, globalAdapterValuesSets); + await _xchangeService.RunValidator(sub.ValidatorId, validatorProperties, xchangeFile); var xchangeId = await _xchangeService.SubmitSubscriptionXchange(sub.Id, xchangeFile, xchangeReferences.ToArray()); diff --git a/SW.Bitween.Api/SW.Bitween.Api.csproj b/SW.Bitween.Api/SW.Bitween.Api.csproj index 5a6e99d7..6ec83588 100644 --- a/SW.Bitween.Api/SW.Bitween.Api.csproj +++ b/SW.Bitween.Api/SW.Bitween.Api.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 SW.Bitween @@ -21,9 +21,18 @@ - + + + + + diff --git a/SW.Bitween.Api/Services/AdapterInvoker.cs b/SW.Bitween.Api/Services/AdapterInvoker.cs new file mode 100644 index 00000000..f310d484 --- /dev/null +++ b/SW.Bitween.Api/Services/AdapterInvoker.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// Runs a handler adapter, whichever kind it is: in-process for a native handler, or through +/// serverless for an uploaded one. +/// +/// +/// The choice between the two is made from the id alone and is identical wherever a handler is +/// invoked, so it lives here once. Kept as the single place that knows the adapter contract — when +/// that contract changes, a copy of this block somewhere else is what gets left behind. +/// +public class AdapterInvoker( + NativeAdapterDiscoveryService nativeAdapterDiscovery, + IServiceProvider serviceProvider) +{ + /// + /// Hands to the handler and returns whatever it produced, which is + /// null for a handler that only consumes. + /// + public async Task Handle(string handlerId, Dictionary handlerProperties, + string correlationId, XchangeFile payload) + { + if (handlerId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) + { + var handler = nativeAdapterDiscovery.GetNativeHandler(handlerId, handlerProperties); + return await handler.Handle(payload); + } + + var serverless = serviceProvider.GetRequiredService(); + await serverless.StartAsync(handlerId, correlationId, handlerProperties); + return await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), payload); + } +} diff --git a/SW.Bitween.Api/Services/AdapterRequirements.cs b/SW.Bitween.Api/Services/AdapterRequirements.cs new file mode 100644 index 00000000..327ddfbe --- /dev/null +++ b/SW.Bitween.Api/Services/AdapterRequirements.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// Which of an adapter's required startup properties a caller failed to supply. +/// +/// Asking this question means knowing whether the adapter runs in-process or in a serverless +/// container, and the answer was written out four times across the subscription validators +/// before this existed — three in Update alone. Create needs the same answer, and six copies +/// of it would be six places for the rule to drift. +/// +/// +public class AdapterRequirements( + NativeAdapterDiscoveryService nativeAdapterDiscovery, + IServiceProvider serviceProvider) +{ + /// Native (native: prefix) or serverless. Null/blank means nothing is missing. + /// What the caller supplied. Blank values count as not supplied. + public async Task> MissingFor(string adapterId, ICollection provided) + { + if (string.IsNullOrEmpty(adapterId)) return Array.Empty(); + + IEnumerable required; + if (adapterId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) + { + required = nativeAdapterDiscovery.GetStartupValues(adapterId) + .Where(p => !p.Value.Optional).Select(p => p.Key); + } + else + { + // Resolved late: starting a serverless adapter is expensive, and most validations + // never reach this branch. + var serverless = serviceProvider.GetRequiredService(); + await serverless.StartAsync(adapterId, null); + required = (await serverless.GetExpectedStartupValues()) + .Where(p => p.Value.Optional == false).Select(p => p.Key); + } + + return required + .ToHashSet(StringComparer.OrdinalIgnoreCase) + .Except((provided ?? Array.Empty()) + .Where(p => !string.IsNullOrEmpty(p.Value)).Select(p => p.Key)) + .ToArray(); + } +} diff --git a/SW.Bitween.Api/Services/AdapterSecretProperties.cs b/SW.Bitween.Api/Services/AdapterSecretProperties.cs new file mode 100644 index 00000000..331f3bde --- /dev/null +++ b/SW.Bitween.Api/Services/AdapterSecretProperties.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// Keeps adapter secrets — an api key, a mail password — out of responses, and puts them back when +/// an unchanged one is saved again. +/// +/// +/// +/// An adapter marks a startup value [Secure], which is what +/// reports. replaces those values with on the way out, and +/// reads the sentinel on the way back in as "keep what is stored" — so a form +/// that only changed the subject line does not overwrite the password with a row of dots. +/// +/// +/// The same sentinel and the same pair of steps already guard subscription adapter properties inside +/// Subscriptions/Get and Subscriptions/Update; this is the reusable form of it. +/// +/// +public class AdapterSecretProperties( + NativeAdapterDiscoveryService nativeAdapterDiscovery, + IServiceProvider serviceProvider) +{ + /// Stands in for a secret value in any response that carries adapter properties. + public const string Sentinel = "__private__"; + + // Describing a serverless adapter means starting it and asking, which is far too expensive to + // repeat per row of a report. Scoped service, so the memo lives exactly as long as one request. + private readonly Dictionary> _described = new(); + + /// + /// Returns a copy with every secret value replaced. Values that are already empty are left + /// alone, so "not set" stays distinguishable from "set but hidden". + /// + public async Task> Mask( + string adapterId, IReadOnlyDictionary properties) + { + if (properties == null || properties.Count == 0) + return properties?.ToDictionary(kv => kv.Key, kv => kv.Value); + + // No adapter to ask about: mask nothing rather than guess. There is also nothing to send + // the properties to, so they cannot be credentials in use. + if (string.IsNullOrEmpty(adapterId)) + return properties.ToDictionary(kv => kv.Key, kv => kv.Value); + + IDictionary startupValues; + try + { + startupValues = await Describe(adapterId); + } + catch + { + // Fail closed: when the adapter cannot be described there is no way to tell which value + // is a secret, and guessing wrong one way leaks it. + return properties.ToDictionary(kv => kv.Key, _ => Sentinel); + } + + return properties.ToDictionary(kv => kv.Key, kv => + startupValues.TryGetValue(kv.Key, out var startupValue) + && startupValue.Private + && !string.IsNullOrEmpty(kv.Value) + ? Sentinel + : kv.Value); + } + + /// + /// Resolves the sentinels in against what is already stored. A + /// sentinel with nothing stored under that key is dropped rather than saved literally. + /// + public static Dictionary Merge( + IReadOnlyDictionary stored, IReadOnlyDictionary incoming) + { + if (incoming == null) return null; + + var result = new Dictionary(); + foreach (var kv in incoming) + { + if (kv.Value != Sentinel) + { + result[kv.Key] = kv.Value; + } + else if (stored != null && stored.TryGetValue(kv.Key, out var storedValue)) + { + result[kv.Key] = storedValue; + } + } + return result; + } + + /// + /// applied to the dictionary the caller already holds. + /// + /// + /// is an immutable value object — every property is init — so a + /// group's properties cannot be swapped for a masked copy. Editing the dictionary in place is + /// the way to reach them without either loosening that contract or rebuilding each group + /// property by property, which would silently drop whatever property is added to it next. + /// + public async Task MaskInPlace(string adapterId, Dictionary properties) + { + if (properties == null || properties.Count == 0) return; + + var masked = await Mask(adapterId, properties); + properties.Clear(); + foreach (var kv in masked) properties[kv.Key] = kv.Value; + } + + /// applied to the dictionary the caller already holds. + public static void MergeInPlace( + IReadOnlyDictionary stored, Dictionary incoming) + { + if (incoming == null || incoming.Count == 0) return; + + var merged = Merge(stored, incoming); + incoming.Clear(); + foreach (var kv in merged) incoming[kv.Key] = kv.Value; + } + + private async Task> Describe(string adapterId) + { + if (_described.TryGetValue(adapterId, out var cached)) return cached; + + IDictionary startupValues; + if (adapterId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) + { + startupValues = nativeAdapterDiscovery.GetStartupValues(adapterId); + } + else + { + var serverless = serviceProvider.GetRequiredService(); + await serverless.StartAsync(adapterId, null); + startupValues = await serverless.GetExpectedStartupValues(); + } + + _described[adapterId] = startupValues; + return startupValues; + } +} diff --git a/SW.Bitween.Api/Services/BitweenOptions.cs b/SW.Bitween.Api/Services/BitweenOptions.cs index a5b05aa3..d9d426a3 100644 --- a/SW.Bitween.Api/Services/BitweenOptions.cs +++ b/SW.Bitween.Api/Services/BitweenOptions.cs @@ -9,15 +9,13 @@ public class BitweenOptions public BitweenOptions() { // AESEncryptionKey = "BitweenS9SecretKey"; - AdapterPath = "./adapters"; + AdapterPath = "adapters"; AdminCredentials = "admin:1234512345"; DocumentPrefix = "temp30/Bitweendocs"; - ClientIpHeaderName = "X-Real-IP"; DatabaseType = "MySql"; AdminDatabaseName = "defaultdb"; ServerlessCommandTimeout = 300; ApiCallSubscriptionResponseAcceptedStatusCode = 202; - ReceiversDelayInSeconds = 63; StorageProvider = "S3"; JwtExpiryMinutes = 60; BusDefaultQueuePrefetch = 12; @@ -29,14 +27,17 @@ public BitweenOptions() public string DatabaseType { get; set; } public string AdminDatabaseName { get; set; } + + /// + /// Cloud-storage key prefix the serverless runner downloads custom adapter packages from + /// ({AdapterPath}/{adapterId}). Passed to ServerlessOptions.AdapterRemotePath. + /// public string AdapterPath { get; set; } public string AdminCredentials { get; set; } public string DocumentPrefix { get; set; } - public string ClientIpHeaderName { get; set; } public int ServerlessCommandTimeout { get; set; } public bool AreXChangeFilesPrivate { get; set; } = false; public int? ApiCallSubscriptionResponseAcceptedStatusCode { get; set; } - public int? ReceiversDelayInSeconds { get; set; } public string StorageProvider { get; set; } @@ -71,12 +72,22 @@ public BitweenOptions() /// public string AzureManagedIdentityClientId { get; set; } + /// + /// Passphrase used to encrypt secret settings before they're stored. Environment-only and + /// never itself a setting — it's what protects the table, so it can't live in it. Without + /// it, secret settings are neither imported nor editable and keep coming from configuration. + /// Rotating it makes anything already stored unreadable. + /// + public string SettingsEncryptionKey { get; set; } + public string RabbitMqManagementUrl { get; set; } public string RabbitMqManagementUsername { get; set; } public string RabbitMqManagementPassword { get; set; } /// - /// License key for the Rebex POP3 library. When not set, the native Rebex POP3 receiver adapter is not registered. + /// License key for the Rebex library the native POP3 and FTP adapters are built on. Those + /// adapters are always registered, so a key stored in Settings takes effect without a + /// restart; while no key is set they're kept out of the adapter pickers instead. /// public string RebexLicenseKey { get; set; } @@ -93,5 +104,18 @@ public BitweenOptions() /// Format: second minute hour dayOfMonth month dayOfWeek /// public string RetryJobCron { get; set; } = "0 * * * * ?"; + + /// + /// How many days to keep ReceiveAttempt rows before ReceiveAttemptCleanupJob + /// deletes them. Matches the scheduler library's own JobExecution retention default. + /// + public int ReceiveAttemptRetentionDays { get; set; } = 30; + + /// + /// Quartz cron expression for ReceiveAttemptCleanupJob. Defaults to daily at 3am — + /// offset from the scheduler library's own cleanup job (2am) so they don't run at once. + /// Format: second minute hour dayOfMonth month dayOfWeek + /// + public string ReceiveAttemptCleanupCron { get; set; } = "0 0 3 * * ?"; } } \ No newline at end of file diff --git a/SW.Bitween.Api/Services/CacheRevokeService.cs b/SW.Bitween.Api/Services/CacheRevokeService.cs index 86e3b958..b499372e 100644 --- a/SW.Bitween.Api/Services/CacheRevokeService.cs +++ b/SW.Bitween.Api/Services/CacheRevokeService.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using SW.Bitween.Services; using SW.PrimitiveTypes; namespace SW.Bitween; @@ -6,15 +7,21 @@ namespace SW.Bitween; public class CacheRevokeService : IListen { private readonly IInfolinkCache _BitweenCache; + private readonly SettingsService _settings; + private readonly BitweenDbContext _dbContext; - public CacheRevokeService(IInfolinkCache BitweenCache) + public CacheRevokeService(IInfolinkCache BitweenCache, SettingsService settings, BitweenDbContext dbContext) { _BitweenCache = BitweenCache; + _settings = settings; + _dbContext = dbContext; } - public Task Process(RevokeCacheMessage message) + public async Task Process(RevokeCacheMessage message) { _BitweenCache.Revoke(); - return Task.CompletedTask; + // Settings live on singletons rather than in the cache, so they need their own refresh: + // this is how an instance picks up a setting changed on a different instance. + await _settings.Reload(_dbContext); } -} \ No newline at end of file +} diff --git a/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs b/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs index e7168dd6..df5be0f7 100644 --- a/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs +++ b/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs @@ -71,8 +71,11 @@ public async Task ListBusGatewayRoutesByDocumentAsync(int doc cachedBusGateways = _cache.Get(nameof(BusGateway)); } + // A deactivated gateway offers no routes, which is the whole of what deactivating + // one means on the bus side: the message still publishes, this gateway just stops + // being one of the places it lands. return cachedBusGateways - .Where(g => g.DocumentId == documentId) + .Where(g => g.DocumentId == documentId && !g.Inactive) .SelectMany(g => g.Routes ?? Enumerable.Empty()) .ToArray(); } @@ -124,11 +127,23 @@ public async Task DocumentByNameAsync(string documentName) string.Equals(d.Name, documentName, StringComparison.CurrentCultureIgnoreCase)); } - public Task BroadcastRevoke() + public async Task BroadcastRevoke() { - using var scope = _ssf.CreateScope(); - var broadcast = scope.ServiceProvider.GetRequiredService(); - return broadcast.Broadcast(new RevokeCacheMessage()); + // This is a best-effort cache-refresh signal, not part of the write + // itself — a dozen command handlers across Subscriptions/WorkGroups/ + // BusGateways/Documents call this right after SaveChangesAsync, and an + // unhandled failure here (e.g. the RabbitMQ connection being down) + // must not turn an already-successful write into a 500 response. + try + { + using var scope = _ssf.CreateScope(); + var broadcast = scope.ServiceProvider.GetRequiredService(); + await broadcast.Broadcast(new RevokeCacheMessage()); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to broadcast cache revoke; cached reads may stay stale until they expire."); + } } public async Task ListWorkGroupsAsync() diff --git a/SW.Bitween.Api/Services/FilterService.cs b/SW.Bitween.Api/Services/FilterService.cs index 6dd8ecf0..9c52ed6e 100644 --- a/SW.Bitween.Api/Services/FilterService.cs +++ b/SW.Bitween.Api/Services/FilterService.cs @@ -38,16 +38,44 @@ public async Task Filter(int documentId, XchangeFile xchangeFile) //TODO check if we need to validate here //if (ppValue is null) // throw new SWValidationException("PROMOTED_PROPERTY_NOT_FOUND", $"The path {pp.Value} is null on the docuemnt"); - filterResult.Properties.Add(pp.Key, ppValue?.ToLower()); + // Stored as the payload sent it. It used to be lower-cased here, which was + // only ever to pair with the lower-cased term in Xchanges/Search — nothing + // matches on this dictionary (match expressions read the payload directly), + // so the one thing it changed was what every screen displays: an order for + // "Acme Retail" listed as "acme retail". Search now lower-cases the column + // instead, which keeps it case-insensitive without rewriting the data. + filterResult.Properties.Add(pp.Key, ppValue); } var subs = await _cache.ListSubscriptionsByDocumentAsync(documentId); var matches = subs.Where(sub => { - // Bus-gateway subscriptions only run via their gateway routes (with the route's - // filter and optional partner), never through the normal auto-match flow. - if (sub.Type == SubscriptionType.BusGateway) + // An integration with an entry point of its own is never started by a document + // merely arriving on its information type — it is started through that entry + // point, which is what decides it should run at all: + // + // BusGateway — its gateway's routes (route filter + optional partner) + // Receiving — its schedule, via ReceivingJob + // GatewayApiCall — a partner calling the gateway it is attached to + // ApiCall — its own partner posting to Xchanges/Update, which runs the + // subscription belonging to the caller (legacy GatewayApiCall) + // + // All four are started by name, through SubmitSubscriptionXchange. Auto-matching + // them as well ran them a second time, on traffic addressed to nobody: a scheduled + // job publishing the very message type it is bound to fed itself forever, and an + // ApiCall integration belonging to one partner ran on another partner's message. + // Both stayed hidden only while those handlers happened to be unreachable. The + // second run also arrived without the partner the entry point would have passed, + // so every {{partner.…}} in its adapters stayed a literal token. + // + // Internal keeps matching. Reacting to a document of its type arriving is the + // whole definition of the type — it has no other trigger. Aggregation is driven + // by AggregationJob. + if (sub.Type is SubscriptionType.BusGateway + or SubscriptionType.Receiving + or SubscriptionType.GatewayApiCall + or SubscriptionType.ApiCall) return false; var exp = sub.BackwardCompatibleMatchExpression(doc); diff --git a/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs b/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs index 3a070a16..4fa61860 100644 --- a/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs +++ b/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs @@ -13,7 +13,8 @@ public class NativeAdapterDiscoveryService( IEnumerable nativeMappers, IEnumerable nativeReceivers, IEnumerable nativeValidators, - IEnumerable nativeAdapters) + IEnumerable nativeAdapters, + BitweenOptions bitweenOptions) { public const string NativePrefix = "native"; public Dictionary GetStartupValues(string adapterId) @@ -148,6 +149,12 @@ public List GetNativeAdapters(string? type) return new List(); } + // Rebex-backed adapters are always registered (the license key is a setting that can + // change at runtime), so they're filtered out here while no key is set rather than + // being offered in a picker where they could only fail. + if (string.IsNullOrWhiteSpace(bitweenOptions.RebexLicenseKey)) + adapters = adapters.Where(a => a is not IRequiresRebexLicense).ToList(); + return adapters.Select(a => a.GetType().Name).ToList(); } private string? GetDefaultValue(PropertyInfo property) diff --git a/SW.Bitween.Api/Services/ReceiveAttemptCleanupJob.cs b/SW.Bitween.Api/Services/ReceiveAttemptCleanupJob.cs new file mode 100644 index 00000000..9abb9bc8 --- /dev/null +++ b/SW.Bitween.Api/Services/ReceiveAttemptCleanupJob.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Scheduler; + +namespace SW.Bitween; + +/// +/// Deletes rows older than +/// . +/// Scheduled via (registered by +/// SchedulerSeedService). +/// +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class ReceiveAttemptCleanupJob(BitweenDbContext dbContext, BitweenOptions options) : IScheduledJob +{ + public async Task Execute() + { + var cutoff = DateTime.UtcNow.AddDays(-options.ReceiveAttemptRetentionDays); + await dbContext.Set() + .Where(a => a.StartedOn < cutoff) + .ExecuteDeleteAsync(); + } +} diff --git a/SW.Bitween.Api/Services/ReceivingJob.cs b/SW.Bitween.Api/Services/ReceivingJob.cs index 927888fc..cad0dfab 100644 --- a/SW.Bitween.Api/Services/ReceivingJob.cs +++ b/SW.Bitween.Api/Services/ReceivingJob.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SW.Bitween.Domain; +using SW.Bitween.Model; using SW.PrimitiveTypes; using SW.Scheduler; using System; @@ -32,16 +33,40 @@ public async Task Execute(ReceivingJobParams jobParams) var isIdle = await runFlagUpdater.MarkAsRunning(rec.Id); if (!isIdle) return; + var startedOn = DateTime.UtcNow; + // Populated as files come in, so a mid-loop failure still leaves the ones that + // did make it through visible on the attempt record rather than orphaned. + var createdExchangeIds = new List(); + + // Advances regardless of outcome: the Quartz trigger fires on its own cron no + // matter what happens below, so "next run" has to track that, not the receive + // step's success — otherwise a receiver that keeps failing freezes ReceiveOn + // in the past forever while the job keeps firing on schedule underneath it. + // Isolated in its own try: a schedule problem is unrelated to receiving and + // must not stop the step below from running. try { - var startupParameters = rec.ReceiverProperties.ToDictionary(); - await RunReceiver(rec.ReceiverId, startupParameters, rec.Id); rec.SetSchedules(); + } + catch (Exception ex) + { + logger.LogError(ex, "Could not advance the schedule for subscription {SubscriptionId}", jobParams.SubscriptionId); + } + + try + { + var globals = await dbContext.Set().ToArrayAsync(); + var startupParameters = rec.ReceiverProperties.ToDictionary().Fill(null, globals); + await RunReceiver(rec.ReceiverId, startupParameters, rec.Id, createdExchangeIds); rec.SetHealth(); + RecordAttempt(rec.Id, startedOn, + createdExchangeIds.Count > 0 ? ReceiveOutcome.Received : ReceiveOutcome.NoNewData, + null, createdExchangeIds); } catch (Exception ex) { rec.SetHealth(ex.ToString()); + RecordAttempt(rec.Id, startedOn, ReceiveOutcome.Failed, ex.ToString(), createdExchangeIds); logger.LogError(ex, "Error processing receiver for subscription {SubscriptionId}", jobParams.SubscriptionId); } finally @@ -52,7 +77,24 @@ public async Task Execute(ReceivingJobParams jobParams) await dbContext.SaveChangesAsync(); } - private async Task RunReceiver(string serverlessId, IDictionary startupParameters, int subId) + private void RecordAttempt( + int subscriptionId, DateTime startedOn, ReceiveOutcome outcome, string errorMessage, + List exchangeIds) + { + dbContext.Add(new ReceiveAttempt + { + SubscriptionId = subscriptionId, + StartedOn = startedOn, + FinishedOn = DateTime.UtcNow, + Outcome = outcome, + ErrorMessage = errorMessage, + ExchangeIds = exchangeIds.ToArray(), + }); + } + + private async Task RunReceiver( + string serverlessId, IDictionary startupParameters, int subId, + List createdExchangeIds) { if (serverlessId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) { @@ -66,7 +108,7 @@ private async Task RunReceiver(string serverlessId, IDictionary { var xchangeFile = await receiver.GetFile(file); logger.LogInformation("Submitting received file for subscriber: '{SubId}'.", subId); - await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); + createdExchangeIds.Add(await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile)); await receiver.DeleteFile(file); } @@ -84,7 +126,7 @@ private async Task RunReceiver(string serverlessId, IDictionary { var xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkReceiver.GetFile), file); logger.LogInformation("Submitting received file for subscriber: '{SubId}'.", subId); - await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); + createdExchangeIds.Add(await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile)); await serverless.InvokeAsync(nameof(IInfolinkReceiver.DeleteFile), file); } diff --git a/SW.Bitween.Api/Services/RetryAlertResolver.cs b/SW.Bitween.Api/Services/RetryAlertResolver.cs new file mode 100644 index 00000000..3e9a95a7 --- /dev/null +++ b/SW.Bitween.Api/Services/RetryAlertResolver.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween; + +/// Where a resolved alert should be delivered, and which level of the hierarchy decided it. +public class RetryAlertTarget +{ + public required string HandlerId { get; init; } + public IReadOnlyDictionary HandlerProperties { get; init; } + + /// Which level won — shown in the UI so a surprising destination can be traced. + public required RetryAlertLevel Level { get; init; } +} + +/// +/// Resolves where a group's exhaustion alert goes, walking from the most specific level to the +/// least: the subscription+group override, then the group, then the policy. +/// +/// +/// +/// A level that overrides replaces the level above rather than merging into it, so +/// whichever level wins must carry the handler and every property it needs. That keeps what the UI +/// shows for a level identical to what actually gets sent. +/// +/// +/// Resolved at send time rather than stored, so editing a policy's default immediately affects +/// everything still inheriting it. +/// +/// +public static class RetryAlertResolver +{ + /// + /// Returns the destination for one subscription's alert in one group, or null when no + /// level configures one or a level explicitly silences it. + /// + /// The subscription+group override, or null if none exists. + /// The matched group. null when the group no longer exists in the policy. + /// + /// The named policy, or null when the subscription uses an inline + /// — those have no policy row, so only the group and the + /// override levels can configure an alert. + /// + public static RetryAlertTarget Resolve(RetryAlertOverride subscriptionOverride, RetryGroup group, + RetryPolicy policy) + { + switch (subscriptionOverride?.AlertMode) + { + case RetryAlertMode.Silent: + return null; + case RetryAlertMode.Send when !string.IsNullOrWhiteSpace(subscriptionOverride.AlertHandlerId): + return new RetryAlertTarget + { + HandlerId = subscriptionOverride.AlertHandlerId, + HandlerProperties = subscriptionOverride.AlertHandlerProperties, + Level = RetryAlertLevel.SubscriptionGroup + }; + } + + switch (group?.AlertMode) + { + case RetryAlertMode.Silent: + return null; + case RetryAlertMode.Send when !string.IsNullOrWhiteSpace(group.AlertHandlerId): + return new RetryAlertTarget + { + HandlerId = group.AlertHandlerId, + HandlerProperties = group.AlertHandlerProperties, + Level = RetryAlertLevel.Group + }; + } + + if (!string.IsNullOrWhiteSpace(policy?.AlertHandlerId)) + return new RetryAlertTarget + { + HandlerId = policy.AlertHandlerId, + HandlerProperties = policy.AlertHandlerProperties, + Level = RetryAlertLevel.Policy + }; + + return null; + } +} diff --git a/SW.Bitween.Api/Services/RetryAlertService.cs b/SW.Bitween.Api/Services/RetryAlertService.cs new file mode 100644 index 00000000..414e3be1 --- /dev/null +++ b/SW.Bitween.Api/Services/RetryAlertService.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// Delivers "retry budget exhausted" alerts, on its own queue. +/// +/// +/// Deliberately a separate consumer rather than another branch inside XchangeService's +/// result handling: alerts go through a customer-configured adapter that may be slow or broken, and +/// on the shared result queue that would hold up — or fail — the ordinary notifiers for the same +/// exchange. Its own means its own queue, and the two fail apart. +/// +public class RetryAlertService( + BitweenDbContext dbContext, + AdapterInvoker adapterInvoker, + ILogger logger) : IConsume +{ + public async Task Process(RetryBudgetExhaustedEvent message) + { + // The bus is at-least-once, and the exhaustion is stamped on the exchange rather than on + // the send, so a redelivery would otherwise email the same alert twice. A *successful* log + // row is the record that it already went out — matching any alert row would let one failed + // send stand in for a delivery and silence every later attempt. The name is checked too, so + // a row written by some other path can never be mistaken for this alert. + var alreadySent = await dbContext.Set() + .AnyAsync(n => n.XchangeId == message.XchangeId + && n.NotifierName == XchangeNotification.RetryBudgetAlertName + && n.Success); + if (alreadySent) return; + + var subscription = await dbContext.Set() + .Include(s => s.RetryPolicy) + .FirstOrDefaultAsync(s => s.Id == message.SubscriptionId); + if (subscription == null) return; + + // An inline custom policy has no policy row, so only the group and override levels of the + // hierarchy can configure an alert for it. + IRetryPolicy policy = subscription.CustomRetryPolicy ?? (IRetryPolicy)subscription.RetryPolicy; + var group = policy?.Groups?.FirstOrDefault(g => g.Id == message.GroupId); + + var subscriptionOverride = await dbContext.Set() + .FirstOrDefaultAsync(o => o.SubscriptionId == message.SubscriptionId + && o.GroupId == message.GroupId); + + var target = RetryAlertResolver.Resolve(subscriptionOverride, group, subscription.RetryPolicy); + if (target == null) return; + + var notification = await BuildNotification(message, subscription); + await Send(target, notification, message.XchangeId); + } + + private async Task BuildNotification( + RetryBudgetExhaustedEvent message, Subscription subscription) + { + var context = await (from xchange in dbContext.Set().AsNoTracking() + where xchange.Id == message.XchangeId + join document in dbContext.Set() on xchange.DocumentId equals document.Id + join result in dbContext.Set() on xchange.Id equals result.Id into xr + from result in xr.DefaultIfEmpty() + select new + { + document.Name, + xchange.CorrelationId, + result.Exception, + result.RetryBlockedReason + }) + .FirstOrDefaultAsync(); + + return new RetryBudgetExhaustedNotification + { + XchangeId = message.XchangeId, + SubscriptionId = message.SubscriptionId, + SubscriptionName = subscription.Name, + DocumentName = context?.Name, + CorrelationId = context?.CorrelationId, + PolicyName = subscription.RetryPolicy?.Name, + GroupName = message.GroupName, + MaxAttemptsTotal = message.MaxAttemptsTotal, + BlockedReason = context?.RetryBlockedReason, + Exception = context?.Exception, + OccurredOn = message.OccurredOn + }; + } + + /// + /// Invokes the resolved handler and records the attempt either way. + /// + /// + /// A throw is logged rather than propagated, and the failure is recorded so someone can answer + /// "did the alert actually go out?". Because the guard above only counts a successful row, a + /// failed send leaves the way open for a redelivery to try again rather than closing it. + /// + private async Task Send(RetryAlertTarget target, RetryBudgetExhaustedNotification notification, + string xchangeId) + { + var handlerProperties = new Dictionary( + target.HandlerProperties ?? new Dictionary()) + { + ["xchangeid"] = xchangeId + }; + + var payload = new XchangeFile(JsonConvert.SerializeObject(notification), xchangeId); + + try + { + await adapterInvoker.Handle(target.HandlerId, handlerProperties, + notification.CorrelationId ?? xchangeId, payload); + + dbContext.Add(XchangeNotification.ForRetryBudgetAlert(xchangeId)); + } + catch (Exception ex) + { + logger.LogError(ex, + "Retry budget alert for xchange {XchangeId} could not be delivered through {HandlerId}.", + xchangeId, target.HandlerId); + dbContext.Add(XchangeNotification.ForRetryBudgetAlert(xchangeId, ex.ToString())); + } + + await dbContext.SaveChangesAsync(); + } +} diff --git a/SW.Bitween.Api/Services/RetryGroupBudget.cs b/SW.Bitween.Api/Services/RetryGroupBudget.cs new file mode 100644 index 00000000..deb10836 --- /dev/null +++ b/SW.Bitween.Api/Services/RetryGroupBudget.cs @@ -0,0 +1,174 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween; + +/// +/// backed by the RetryGroupUsage table and scoped to one +/// integration, so a policy template shared by many integrations gives each its own total +/// instead of letting one noisy integration spend everyone's budget. +/// +public class RetryGroupBudget( + BitweenDbContext dbContext, + IServiceProvider serviceProvider, + int subscriptionId) : IRetryGroupBudget +{ + /// + /// + /// + /// The claim is a single conditional UPDATE, so the check and the increment happen as + /// one database operation. Bitween runs several instances, and a read-then-write would let two + /// simultaneous failures both observe the last free slot and both retry, exceeding the cap. + /// + /// + /// Because it commits on its own rather than with the caller's SaveChangesAsync, a slot + /// can be charged for a retry that is never scheduled if that later save fails. That errs + /// toward retrying less than the cap allows, and RetryPolicies/resetusage can return it. + /// The alternative — an explicit transaction spanning the caller's save — would publish this + /// xchange's bus events before the commit, since BitweenDbContext dispatches domain events + /// inside SaveChangesAsync. + /// + /// + public async Task TryConsume(Guid groupId, int maxAttemptsTotal) + { + // A group configured to allow no retries at all has no budget to exhaust, so it never + // alerts — otherwise every single failure under it would raise one. + if (maxAttemptsTotal <= 0) return RetryBudgetClaim.Denied; + + if (await TryIncrement(dbContext, groupId, maxAttemptsTotal)) return RetryBudgetClaim.Allowed; + + // Nothing was updated: either the ceiling is reached, or this integration and group have + // never failed before and so have no row yet. + var exists = await dbContext.Set() + .AnyAsync(u => u.SubscriptionId == subscriptionId && u.GroupId == groupId); + if (exists) return await ClaimExhaustionAlert(groupId, maxAttemptsTotal); + + // Create that first row on its own context so it commits independently of whatever the + // caller still has pending. Losing this race is harmless: the primary key rejects the + // duplicate and the conditional increment is then applied to the winner's row. + using var scope = serviceProvider.CreateScope(); + var isolated = scope.ServiceProvider.GetRequiredService(); + + isolated.Add(new RetryGroupUsage + { + SubscriptionId = subscriptionId, + GroupId = groupId, + AttemptsUsed = 1, + LastAttemptOn = DateTime.UtcNow + }); + + try + { + await isolated.SaveChangesAsync(); + return RetryBudgetClaim.Allowed; + } + catch (DbUpdateException) + { + // The winner of the insert race already holds a row, so this is the ordinary + // increment path again — including the case where their row is already full. + return await TryIncrement(dbContext, groupId, maxAttemptsTotal) + ? RetryBudgetClaim.Allowed + : await ClaimExhaustionAlert(groupId, maxAttemptsTotal); + } + } + + /// + /// Lifts this integration's group budgets that have run out, because it has just succeeded. + /// + /// + /// + /// An exhausted total is a statement about a downstream that was failing, and one success says + /// that is no longer true. Nothing else can say it: an exhausted group schedules no further + /// retries, so no retry will ever succeed to report the recovery — only ordinary traffic getting + /// through can. Without this, one bad afternoon stops retrying for good until somebody notices + /// and resets it by hand. + /// + /// + /// Only budgets that are actually used up. A partly-spent total is left alone. + /// The cap exists to stop a flaky downstream being hammered, and that is precisely a downstream + /// where some messages succeed and others fail — crediting the total back on every ordinary + /// success would mean such a subscription never reaches its cap at all. + /// + /// + /// keeps this from erasing a charge it never saw. Bitween runs + /// several instances, so a failure can claim a slot while this success is still being processed; + /// deleting that row would hand back a slot already spent and let the group exceed its total. + /// Only rows whose last attempt predates the success are released. + /// + /// + /// Deleting a row re-arms the exhaustion alert along with the budget, so if the total runs out + /// again somebody is told again rather than the second outage passing in silence. + /// + /// + /// How many group budgets were released. + public async Task ReleaseExhaustedBudgets(DateTime succeededFrom) + { + // Cheapest question first, and for almost every success the answer ends it here: a + // subscription that has never spent a retry has no row, and must not pay for a policy load + // or a write on the strength of having worked. + var spent = await dbContext.Set().AsNoTracking() + .Where(u => u.SubscriptionId == subscriptionId) + .Select(u => new { u.GroupId, u.AttemptsUsed }) + .ToListAsync(); + if (spent.Count == 0) return 0; + + var subscription = await dbContext.Set().AsNoTracking() + .Include(s => s.RetryPolicy) + .FirstOrDefaultAsync(s => s.Id == subscriptionId); + + IRetryPolicy policy = subscription?.CustomRetryPolicy ?? (IRetryPolicy)subscription?.RetryPolicy; + if (policy?.Groups == null) return 0; + + // A group whose total is gone from the policy is left to Update and Delete to clean up, which + // they already do — releasing it here would be guessing at a cap that no longer exists. + var exhausted = spent + .Where(u => policy.Groups.Any(g => g.Id == u.GroupId + && g.Budget is { MaxAttemptsTotal: > 0 } + && u.AttemptsUsed >= g.Budget.MaxAttemptsTotal)) + .Select(u => u.GroupId) + .ToList(); + if (exhausted.Count == 0) return 0; + + return await dbContext.Set() + .Where(u => u.SubscriptionId == subscriptionId + && exhausted.Contains(u.GroupId) + && u.LastAttemptOn < succeededFrom) + .ExecuteDeleteAsync(); + } + + /// + /// Takes responsibility for alerting that this integration's budget for the group is spent. + /// + /// + /// One conditional UPDATE for the same reason the increment is one: several instances can + /// discover the empty budget at the same moment, and a read-then-write would let each of them + /// decide it was the first. Exactly one caller updates a row, so exactly one alert is raised — + /// and because Reset deletes the row outright, clearing a budget re-arms the alert with it. + /// + private async Task ClaimExhaustionAlert(Guid groupId, int maxAttemptsTotal) + { + var claimed = await dbContext.Set() + .Where(u => u.SubscriptionId == subscriptionId + && u.GroupId == groupId + && u.AttemptsUsed >= maxAttemptsTotal + && u.ExhaustedNotifiedOn == null) + .ExecuteUpdateAsync(s => s + .SetProperty(u => u.ExhaustedNotifiedOn, _ => DateTime.UtcNow)) > 0; + + return claimed ? RetryBudgetClaim.DeniedAndJustExhausted : RetryBudgetClaim.Denied; + } + + private async Task TryIncrement(BitweenDbContext db, Guid groupId, int maxAttemptsTotal) => + await db.Set() + .Where(u => u.SubscriptionId == subscriptionId + && u.GroupId == groupId + && u.AttemptsUsed < maxAttemptsTotal) + .ExecuteUpdateAsync(s => s + .SetProperty(u => u.AttemptsUsed, u => u.AttemptsUsed + 1) + .SetProperty(u => u.LastAttemptOn, _ => DateTime.UtcNow)) > 0; +} diff --git a/SW.Bitween.Api/Services/RetryJob.cs b/SW.Bitween.Api/Services/RetryJob.cs index f371b4cd..1b3400c9 100644 --- a/SW.Bitween.Api/Services/RetryJob.cs +++ b/SW.Bitween.Api/Services/RetryJob.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using SW.Bitween.Domain; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -13,23 +14,62 @@ namespace SW.Bitween; /// Polls for due records and re-submits the failed Xchanges. /// Scheduled via (registered by SchedulerSeedService). /// +/// +/// Works through every retry that is already due rather than a hundred a minute, and commits one row +/// at a time. Committing the batch in one go meant a single row that could not be carried out +/// discarded the work of all the others and left their schedules in place, so the same batch came +/// back a minute later and failed the same way — no retry would ever have run again. +/// [ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] -public class RetryJob(BitweenDbContext dbContext, XchangeService xchangeService) : IScheduledJob +public class RetryJob(BitweenDbContext dbContext, XchangeService xchangeService, ILogger logger) + : IScheduledJob { private const int BatchSize = 100; public async Task Execute() { - var ready = await dbContext.Set() - .Where(r => r.On <= DateTime.UtcNow) - .Take(BatchSize) - .ToListAsync(); + // Fixed before the first batch: a retry scheduled while this run is working belongs to the next + // tick, otherwise a fast-failing subscription could keep this run going indefinitely. + var due = DateTime.UtcNow; - foreach (var delayedRetry in ready) + while (true) { - await xchangeService.ExecuteDelayedRetry(delayedRetry); - } + var ready = await dbContext.Set() + .Where(r => r.On <= due) + .OrderBy(r => r.On) + .Take(BatchSize) + .ToListAsync(); + + if (ready.Count == 0) return; + + foreach (var delayedRetry in ready) + { + try + { + await xchangeService.ExecuteDelayedRetry(delayedRetry); + await dbContext.SaveChangesAsync(); + } + catch (Exception ex) + { + // Not "dropped": SaveChangesAsync commits before it publishes, so a failure in the + // publish leaves the replacement exchange committed and only its announcement + // missing. Saying the retry was dropped would send whoever reads this looking for + // an exchange that does exist. + logger.LogError(ex, + "The scheduled retry of xchange {XchangeId} did not complete; clearing its " + + "schedule so the queue keeps draining.", delayedRetry.Id); - await dbContext.SaveChangesAsync(); + // Whatever the failed run left staged goes first — saving it would commit the very + // changes that failing was meant to prevent. + dbContext.ChangeTracker.Clear(); + + // Every row leaves the queue one way or another, which is what stops the loop above + // from meeting the same row again and turning the drain into a spin. + await dbContext.Set() + .Where(r => r.Id == delayedRetry.Id) + .ExecuteDeleteAsync(); + } + } + } } } diff --git a/SW.Bitween.Api/Services/RetryUsageReport.cs b/SW.Bitween.Api/Services/RetryUsageReport.cs new file mode 100644 index 00000000..3fd4d6a3 --- /dev/null +++ b/SW.Bitween.Api/Services/RetryUsageReport.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween; + +/// +/// Builds the state of subscription-and-group pairs — spent budget, and where the pair's +/// budget-exhausted alert would go. +/// +/// +/// Shared because the same pairs are asked about from two directions: a policy wants every +/// subscription using it, and a subscription wants its own, whether its policy is a shared one or an +/// inline CustomRetryPolicy that no policy id can reach. Two copies of alert resolution and +/// secret masking would drift, and the half that drifted would be the half that leaks. +/// +public class RetryUsageReport(BitweenDbContext dbContext, AdapterSecretProperties secrets) +{ + /// + /// One row per subscription and per group that could actually exhaust. + /// + /// The pairs' subscriptions, with the names to report. + /// Every group of the applicable policy; the unusable ones are dropped here. + /// + /// The shared policy, or null for an inline one — which has no row to carry a + /// policy-level alert, so those pairs can only resolve to a group or an override. + /// + public async Task> Build( + IReadOnlyList<(int Id, string Name)> subscriptions, + IReadOnlyList allGroups, + RetryPolicy policy) + { + // Only groups that allow retries have a budget to spend — and a group that can never spend + // one can never exhaust it, so it can never alert either. Listing those would invite + // configuring an alert that cannot fire. A ceiling of zero counts as "never": TryConsume + // denies it outright rather than claiming and exhausting it. + var groups = allGroups.Where(g => g.Budget is { MaxAttemptsTotal: > 0 }).ToList(); + if (groups.Count == 0 || subscriptions.Count == 0) return []; + + var subscriptionIds = subscriptions.Select(s => s.Id).ToList(); + + var usages = await dbContext.Set().AsNoTracking() + .Where(u => subscriptionIds.Contains(u.SubscriptionId)) + .ToListAsync(); + + var overrides = await dbContext.Set().AsNoTracking() + .Where(o => subscriptionIds.Contains(o.SubscriptionId)) + .ToListAsync(); + + // What became of the alerts that were raised. The counter records only that one was + // claimed — claiming is what stops a redelivery sending it twice, and it happens before + // the send is tried — so whether anyone was actually told lives here instead, in the + // delivery log. The pair is reconstructed the same way the alert reached it: the exchange + // names the integration, its result names the group. + var alertLog = await ( + from notification in dbContext.Set().AsNoTracking() + where notification.NotifierName == XchangeNotification.RetryBudgetAlertName + join xchange in dbContext.Set().AsNoTracking() + on notification.XchangeId equals xchange.Id + join result in dbContext.Set().AsNoTracking() + on notification.XchangeId equals result.Id + where xchange.SubscriptionId != null + && subscriptionIds.Contains(xchange.SubscriptionId.Value) + && result.RetryGroupId != null + select new + { + SubscriptionId = xchange.SubscriptionId!.Value, + GroupId = result.RetryGroupId!.Value, + notification.Success, + notification.Exception, + notification.FinishedOn + }) + .ToListAsync(); + + // One success is delivery, however many failures preceded it — the same rule the sender + // itself applies when it decides an alert has already gone out. Failing that, the most + // recent failure is the outcome. + var alertByPair = alertLog + .GroupBy(a => (a.SubscriptionId, a.GroupId)) + .ToDictionary( + g => g.Key, + g => g.OrderByDescending(a => a.Success).ThenByDescending(a => a.FinishedOn).First()); + + // Keyed once rather than scanned per pair: both lists are already keyed by exactly this + // pair, and a policy shared by many subscriptions turns the scan into the cost of the + // whole request. + var usageByPair = usages.ToDictionary(u => (u.SubscriptionId, u.GroupId)); + var overrideByPair = overrides.ToDictionary(o => (o.SubscriptionId, o.GroupId)); + + var rows = new List(); + + foreach (var subscription in subscriptions) + foreach (var group in groups) + { + usageByPair.TryGetValue((subscription.Id, group.Id), out var usage); + overrideByPair.TryGetValue((subscription.Id, group.Id), out var subscriptionOverride); + alertByPair.TryGetValue((subscription.Id, group.Id), out var alert); + + var target = RetryAlertResolver.Resolve(subscriptionOverride, group, policy); + + // Mirrors the order the resolver walks, so the reported reason is the level that + // actually decided: an override silences before the group is consulted at all. + var silencedAt = subscriptionOverride?.AlertMode == RetryAlertMode.Silent + ? RetryAlertLevel.SubscriptionGroup + : group.AlertMode == RetryAlertMode.Silent + ? RetryAlertLevel.Group + : (RetryAlertLevel?)null; + + rows.Add(new RetryGroupUsageRow + { + SubscriptionId = subscription.Id, + SubscriptionName = subscription.Name, + GroupId = group.Id, + GroupName = group.Name, + AttemptsUsed = usage?.AttemptsUsed ?? 0, + MaxAttemptsTotal = group.Budget!.MaxAttemptsTotal, + Exhausted = usage != null && usage.AttemptsUsed >= group.Budget.MaxAttemptsTotal, + LastAttemptOn = usage?.LastAttemptOn, + ExhaustedNotifiedOn = usage?.ExhaustedNotifiedOn, + // Only meaningful once an alert has been claimed. A delivery row surviving from + // before a reset would otherwise report an outcome for an alert that has since + // been re-armed and not raised again. + AlertDelivered = usage?.ExhaustedNotifiedOn == null ? null : alert?.Success, + AlertError = usage?.ExhaustedNotifiedOn != null && alert is { Success: false } + ? alert.Exception + : null, + AlertMode = subscriptionOverride?.AlertMode ?? RetryAlertMode.Inherit, + OverrideHandlerId = subscriptionOverride?.AlertHandlerId, + OverrideHandlerProperties = await secrets.Mask( + subscriptionOverride?.AlertHandlerId, subscriptionOverride?.AlertHandlerProperties), + ResolvedHandlerId = target?.HandlerId, + ResolvedHandlerProperties = await secrets.Mask( + target?.HandlerId, target?.HandlerProperties), + ResolvedFrom = target?.Level, + SilencedAt = target == null ? silencedAt : null + }); + } + + return rows + // Worst first: stopped retrying, then alerting nowhere, then whatever has spent most. + .OrderByDescending(r => r.Exhausted) + .ThenBy(r => r.ResolvedHandlerId != null) + .ThenByDescending(r => r.AttemptsUsed) + .ThenBy(r => r.SubscriptionName) + .ThenBy(r => r.GroupName) + .ToList(); + } +} diff --git a/SW.Bitween.Api/Services/SchedulerSeedService.cs b/SW.Bitween.Api/Services/SchedulerSeedService.cs index 056d42e7..e895bd64 100644 --- a/SW.Bitween.Api/Services/SchedulerSeedService.cs +++ b/SW.Bitween.Api/Services/SchedulerSeedService.cs @@ -25,6 +25,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var options = scope.ServiceProvider.GetRequiredService(); await scheduleRepo.Schedule(options.RetryJobCron); + await scheduleRepo.Schedule(options.ReceiveAttemptCleanupCron); var subscriptions = await dbContext.Set() .Where(s => diff --git a/SW.Bitween.Api/Services/Settings/SettingsCatalog.cs b/SW.Bitween.Api/Services/Settings/SettingsCatalog.cs new file mode 100644 index 00000000..2a4f94f5 --- /dev/null +++ b/SW.Bitween.Api/Services/Settings/SettingsCatalog.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Quartz; +using SW.Scheduler; + +namespace SW.Bitween.Services; + +public enum SettingKind +{ + String, + Number, + Boolean, + Color +} + +/// What the UI is allowed to do with a setting. +public enum SettingAccess +{ + /// Stored in the Settings table and changeable from the UI. + Editable, + + /// + /// Environment-owned: shown with its current value so an administrator can see what this + /// instance is running on, but not changeable here because it's read once at startup. + /// + ReadOnly, + + /// + /// Environment-owned and private: only whether a value is set is reported, never the value — + /// for credentials and keys that would be pointless to leak into the browser. + /// + Presence +} + +/// The two option singletons a setting can read from and write to. +public sealed record SettingsTarget(BitweenOptions Bitween, ThemeOptions Theme); + +/// +/// Everything known about one setting except its value: how to show it, and how to read and +/// write it on the live options singletons. +/// +public sealed record SettingDefinition( + string Key, + string Section, + string Label, + string Description, + SettingKind Kind, + bool Secret, + Func Read, + Action Write) +{ + /// Editable unless a definition says otherwise — see . + public SettingAccess Access { get; init; } = SettingAccess.Editable; + + /// + /// Extra work needed to make a new value take effect, where assigning the property isn't + /// enough — rescheduling a job, re-declaring queues. Runs on the instance that saved it, + /// after the value has been applied. + /// + public Func OnChange { get; init; } + + /// Only editable settings get a row; the rest are read straight off the options. + public bool Stored => Access == SettingAccess.Editable; +} + +/// +/// Every setting the settings page shows, and — for the editable ones — the only keys the +/// Settings table ever holds. +/// +/// The membership rule is about , not about being listed here. A +/// setting is editable only if every consumer reads it from the options singleton per call, +/// so a change takes effect immediately. Anything captured once during +/// Startup.ConfigureServices — the bus, CORS, storage, JWT signing, the DB provider — is +/// environment-owned and appears as (its value shown) or +/// (only whether it's set), so an administrator can see what +/// the instance is running on without being offered an edit that couldn't work. +/// +/// +/// Read-only and presence settings never get a row: they're read straight off the options object, +/// which configuration bound at startup. +/// +/// +public static class SettingsCatalog +{ + public static readonly IReadOnlyList All = + [ + // ——— Documents & storage ——— + new("Bitween.AreXChangeFilesPrivate", "Documents & storage", + "Keep exchange files private", + "Turn this on if you want generated exchange files kept private and served through short-lived signed links instead of public URLs.", + SettingKind.Boolean, false, + t => Str(t.Bitween.AreXChangeFilesPrivate), + (t, v) => t.Bitween.AreXChangeFilesPrivate = Bool(v)), + + View("Bitween.DocumentPrefix", "Documents & storage", "Document prefix", + "The cloud-storage key prefix every exchange document is written under. Fixed per environment — changing it would leave everything already stored unreachable.", + SettingKind.String, t => t.Bitween.DocumentPrefix), + + // ——— API behavior ——— + new("Bitween.ApiCallSubscriptionResponseAcceptedStatusCode", "API behavior", + "Accepted response status code", + "Change this if a partner's API expects a different HTTP status code (instead of 202 Accepted) when their request has been queued for async processing.", + SettingKind.Number, false, + t => t.Bitween.ApiCallSubscriptionResponseAcceptedStatusCode?.ToString(CultureInfo.InvariantCulture), + (t, v) => t.Bitween.ApiCallSubscriptionResponseAcceptedStatusCode = NullableInt(v)), + + new("Bitween.JwtExpiryMinutes", "API behavior", + "Sign-in session length (minutes)", + "Shorten this for tighter session security, or lengthen it if teammates are being signed out more often than you'd like. Applies to sessions started after the change.", + SettingKind.Number, false, + t => t.Bitween.JwtExpiryMinutes.ToString(CultureInfo.InvariantCulture), + (t, v) => t.Bitween.JwtExpiryMinutes = Int(v)), + + View("Bitween.CorsOrigins", "API behavior", "Allowed browser origins", + "The origins allowed to call this API with cookies attached. Read once when the CORS policy is built at startup.", + SettingKind.String, t => Join(t.Bitween.CorsOrigins)), + + // ——— Single sign-on (Microsoft) ——— + // Not secrets: these are public client identifiers, and the [Unprotect] Config endpoint + // already serves them to anonymous visitors so the login page can offer the MS button. + new("Bitween.MsalClientId", "Single sign-on (Microsoft)", + "Azure AD client ID", + "Add this — together with the tenant ID and redirect URI below — if you want to let teammates sign in with a Microsoft account. All three are required for Microsoft sign-in to turn on.", + SettingKind.String, false, + t => t.Bitween.MsalClientId, + (t, v) => t.Bitween.MsalClientId = v), + + new("Bitween.MsalTenantId", "Single sign-on (Microsoft)", + "Azure AD tenant ID", + "The Azure AD tenant Microsoft sign-in is restricted to. Required alongside the client ID and redirect URI.", + SettingKind.String, false, + t => t.Bitween.MsalTenantId, + (t, v) => t.Bitween.MsalTenantId = v), + + new("Bitween.MsalRedirectUri", "Single sign-on (Microsoft)", + "Azure AD redirect URI", + "The URL Azure AD sends users back to after signing in — must match the redirect URI registered on the Azure AD app. Required alongside the client ID and tenant ID.", + SettingKind.String, false, + t => t.Bitween.MsalRedirectUri, + (t, v) => t.Bitween.MsalRedirectUri = v), + + new("Bitween.DisableEmailPasswordLogin", "Single sign-on (Microsoft)", + "Microsoft sign-in only", + "Turn this on to stop anyone signing in with an email and password — the sign-in page then offers Microsoft alone, and new teammates are added without a password. Only do this once Microsoft sign-in above is working, or nobody will be able to get in.", + SettingKind.Boolean, false, + t => Str(t.Bitween.DisableEmailPasswordLogin), + (t, v) => t.Bitween.DisableEmailPasswordLogin = Bool(v)), + + // ——— Adapters ——— + new("Bitween.RebexLicenseKey", "Adapters", + "Rebex license key", + "Add this if you want the native POP3 and FTP adapters, which are built on the Rebex library — without a key they aren't offered when picking a receiver or handler.", + SettingKind.String, true, + t => t.Bitween.RebexLicenseKey, + (t, v) => t.Bitween.RebexLicenseKey = v), + + View("Bitween.AdapterPath", "Adapters", "Custom adapter path", + "The cloud-storage key prefix custom adapter packages are downloaded from. Handed to the serverless runner when it's configured at startup.", + SettingKind.String, t => t.Bitween.AdapterPath), + + View("Bitween.ServerlessCommandTimeout", "Adapters", "Custom adapter timeout (seconds)", + "How long a custom adapter may run before the serverless runner gives up on it.", + SettingKind.Number, t => t.Bitween.ServerlessCommandTimeout.ToString(CultureInfo.InvariantCulture)), + + // ——— Reliability & jobs ——— + new("Bitween.RetryJobCron", "Reliability & jobs", + "Retry poll schedule", + "How often Bitween looks for exchanges whose scheduled retry has come due, as a cron expression: second minute hour day-of-month month day-of-week. Saving re-schedules the job straight away.", + SettingKind.String, false, + t => t.Bitween.RetryJobCron, + (t, v) => t.Bitween.RetryJobCron = Cron(v)) + { + // Assigning the property isn't enough here: the trigger already lives in the Quartz + // store, so it has to be replaced. Schedule() reads the value we just applied. + OnChange = sp => sp.GetRequiredService() + .Schedule(sp.GetRequiredService().RetryJobCron) + }, + + // ——— Messaging ——— + // All environment-owned: the bus connection and its queue topology are built once, during + // startup, so nothing here could take effect on a running instance. + View("Bitween.QueuePrefix", "Messaging", "Queue name prefix", + "Prefixed to every queue this instance declares, which is what keeps two Bitween deployments on one RabbitMQ from consuming each other's messages.", + SettingKind.String, t => t.Bitween.QueuePrefix), + + View("Bitween.BusDefaultQueuePrefetch", "Messaging", "Default queue prefetch", + "How many messages a consumer may hold unacknowledged by default. A work group can override it for its own queue.", + SettingKind.Number, t => t.Bitween.BusDefaultQueuePrefetch?.ToString(CultureInfo.InvariantCulture)), + + View("Bitween.ConsumeLegacyEventMessages", "Messaging", "Consume legacy event messages", + "Whether this instance also drains the five queues named after exchange events, which an older Bitween published to before work groups existed. Nothing publishes to them today, so this is only for finishing off messages left behind by an upgrade.", + SettingKind.Boolean, t => Str(t.Bitween.ConsumeLegacyEventMessages)), + + Presence("Bitween.RabbitMqManagementUrl", "Messaging", "RabbitMQ management URL", + "The management API queue health is read from. All three management values are needed before queue depths can be shown.", + t => t.Bitween.RabbitMqManagementUrl), + + Presence("Bitween.RabbitMqManagementUsername", "Messaging", "RabbitMQ management username", + "The account queue health reads with. Required alongside the URL and password.", + t => t.Bitween.RabbitMqManagementUsername), + + Presence("Bitween.RabbitMqManagementPassword", "Messaging", "RabbitMQ management password", + "The password for the management account. Required alongside the URL and username.", + t => t.Bitween.RabbitMqManagementPassword), + + // ——— Database ——— + View("Bitween.UseAzureManagedIdentity", "Database", "Use Azure managed identity", + "Whether database connections authenticate with an Azure managed identity instead of a password in the connection string.", + SettingKind.Boolean, t => Str(t.Bitween.UseAzureManagedIdentity)), + + Presence("Bitween.AzureManagedIdentityClientId", "Database", "Managed identity client ID", + "Set only when a user-assigned identity is used; left unset, the system-assigned identity is.", + t => t.Bitween.AzureManagedIdentityClientId), + + // ——— Security ——— + Presence("Bitween.SettingsEncryptionKey", "Security", "Settings encryption key", + "Encrypts secret settings before they're stored. Without it, a secret can't be saved here at all and stays environment-only.", + t => t.Bitween.SettingsEncryptionKey), + + // ——— Brand & theme ——— + new("Theme.PrimaryColor", "Brand & theme", + "Primary color", + "Re-brands the whole app's accent color — buttons, links, active nav, focus rings — instantly, without waiting on a deploy.", + SettingKind.Color, false, + t => t.Theme.PrimaryColor, + (t, v) => t.Theme.PrimaryColor = v), + + new("Theme.CompanyName", "Brand & theme", + "Company name", + "Shown in the footer and used in a few page titles.", + SettingKind.String, false, + t => t.Theme.CompanyName, + (t, v) => t.Theme.CompanyName = v), + + new("Theme.TabTitle", "Brand & theme", + "Browser tab title", + "What shows in the browser tab.", + SettingKind.String, false, + t => t.Theme.TabTitle, + (t, v) => t.Theme.TabTitle = v), + + new("Theme.TabIcon", "Brand & theme", + "Favicon URL", + "The icon shown in the browser tab. Paste a URL to an .ico, .svg or .png.", + SettingKind.String, false, + t => t.Theme.TabIcon, + (t, v) => t.Theme.TabIcon = v), + + new("Theme.LoginLogo", "Brand & theme", + "Sign-in page logo", + "The logo shown above the sign-in form.", + SettingKind.String, false, + t => t.Theme.LoginLogo, + (t, v) => t.Theme.LoginLogo = v), + + new("Theme.BitweenLogo", "Brand & theme", + "Sidebar logo", + "The full logo shown at the top of the sidebar.", + SettingKind.String, false, + t => t.Theme.BitweenLogo, + (t, v) => t.Theme.BitweenLogo = v), + + // Theme.BitweenIcon is deliberately absent: it configured the icon for a + // collapsed, icon-only sidebar, and this UI has no such mode. Leaving it + // editable meant a setting that silently did nothing. The ThemeOptions + // property stays so existing appsettings keep binding. + + new("Theme.BitweenHeaderIcon", "Brand & theme", + "Mobile header icon", + "Shown instead of the sidebar logo in the narrow top bar on phones. Leave it as-is to reuse the sidebar logo.", + SettingKind.String, false, + t => t.Theme.BitweenHeaderIcon, + (t, v) => t.Theme.BitweenHeaderIcon = v), + + new("Theme.BitweenText", "Brand & theme", + "Sign-in page blurb", + "The marketing description shown beside the sign-in form.", + SettingKind.String, false, + t => t.Theme.BitweenText, + (t, v) => t.Theme.BitweenText = v), + + new("Theme.ShowFooter", "Brand & theme", + "Show the footer", + "Turn this off to hide the footer everywhere — the copyright line and the website / LinkedIn / GitHub links below every page.", + SettingKind.Boolean, false, + t => Str(t.Theme.ShowFooter), + (t, v) => t.Theme.ShowFooter = Bool(v)), + + new("Theme.LinkedinLink", "Brand & theme", + "LinkedIn link", + "Add this if you want a LinkedIn link in the footer — leave blank to hide it.", + SettingKind.String, false, + t => t.Theme.LinkedinLink, + (t, v) => t.Theme.LinkedinLink = v), + + new("Theme.GithubLink", "Brand & theme", + "GitHub link", + "Add this if you want a GitHub link in the footer — leave blank to hide it.", + SettingKind.String, false, + t => t.Theme.GithubLink, + (t, v) => t.Theme.GithubLink = v), + + new("Theme.WebsiteLink", "Brand & theme", + "Website link", + "Add this if you want a company website link in the footer — leave blank to hide it.", + SettingKind.String, false, + t => t.Theme.WebsiteLink, + (t, v) => t.Theme.WebsiteLink = v), + + new("Theme.AllRightsReserved", "Brand & theme", + "Copyright notice", + "The copyright notice text shown in the footer.", + SettingKind.String, false, + t => t.Theme.AllRightsReserved, + (t, v) => t.Theme.AllRightsReserved = v), + + new("Theme.CopyRightsIcon", "Brand & theme", + "Copyright symbol", + "The symbol shown before the copyright notice.", + SettingKind.String, false, + t => t.Theme.CopyRightsIcon, + (t, v) => t.Theme.CopyRightsIcon = v) + ]; + + private static readonly Dictionary ByKey = + All.ToDictionary(d => d.Key, StringComparer.OrdinalIgnoreCase); + + public static SettingDefinition Find(string key) => + key is not null && ByKey.TryGetValue(key, out var definition) ? definition : null; + + /// + /// An environment value the UI shows but can't change. No Write: there's nowhere to + /// write it to that would matter, since whoever reads it did so at startup. + /// + private static SettingDefinition View(string key, string section, string label, string description, + SettingKind kind, Func read) => + new(key, section, label, description, kind, false, read, null) { Access = SettingAccess.ReadOnly }; + + /// An environment value reported only as set or not set, never by its content. + private static SettingDefinition Presence(string key, string section, string label, string description, + Func read) => + new(key, section, label, description, SettingKind.String, false, read, null) + { Access = SettingAccess.Presence }; + + private static string Str(bool value) => value ? "true" : "false"; + + private static string Join(string[] values) => + values is null ? string.Empty : string.Join(", ", values); + + /// + /// Rejected here rather than by the scheduler, because a bad expression that reached the table + /// would throw on the next boot — inside the background service that seeds every subscription's + /// schedule, taking all of them down with it. + /// + private static string Cron(string value) => + CronExpression.IsValidExpression(value) + ? value + : throw new FormatException($"'{value}' is not a valid cron expression."); + + /// Anything but an explicit "true" is off — matches how the UI posts checkboxes. + private static bool Bool(string value) => string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + + private static int Int(string value) => + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) + ? parsed + : throw new FormatException($"'{value}' is not a whole number."); + + private static int? NullableInt(string value) => + string.IsNullOrWhiteSpace(value) ? null : Int(value); +} diff --git a/SW.Bitween.Api/Services/Settings/SettingsHostExtensions.cs b/SW.Bitween.Api/Services/Settings/SettingsHostExtensions.cs new file mode 100644 index 00000000..f486b340 --- /dev/null +++ b/SW.Bitween.Api/Services/Settings/SettingsHostExtensions.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace SW.Bitween.Services; + +public static class SettingsHostExtensions +{ + /// + /// Hands configuration over to the Settings table and applies what's stored, before the app + /// starts serving. Any key without a row yet is imported from configuration once; after that + /// the table is the only source. Runs after MigrateDatabase so the table is guaranteed + /// to exist; an unreachable database is logged and the app boots on its configured values + /// rather than failing to start. + /// + public static IHost ApplyStoredSettings(this IHost host) + { + using var scope = host.Services.CreateScope(); + var settings = scope.ServiceProvider.GetRequiredService(); + + try + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + settings.ImportMissing(dbContext).GetAwaiter().GetResult(); + settings.Reload(dbContext).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + scope.ServiceProvider.GetRequiredService>() + .LogError(ex, "Could not load stored settings; starting on configured values only."); + } + + return host; + } +} diff --git a/SW.Bitween.Api/Services/Settings/SettingsProtector.cs b/SW.Bitween.Api/Services/Settings/SettingsProtector.cs new file mode 100644 index 00000000..0e5994fb --- /dev/null +++ b/SW.Bitween.Api/Services/Settings/SettingsProtector.cs @@ -0,0 +1,87 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace SW.Bitween.Services; + +/// +/// Encrypts secret settings before they're stored, so a database dump or backup never carries a +/// license key in the clear. AES-GCM with a fresh salt and nonce per value: encrypting the same +/// key twice produces different ciphertext, and tampering fails the authentication tag rather +/// than decrypting to garbage. +/// +/// The passphrase comes from — configuration +/// only, never the Settings table. When it isn't set, is false and +/// secret settings stay out of the table entirely. +/// +/// +public class SettingsProtector +{ + private const string Prefix = "enc.v1:"; + private const int SaltBytes = 16; + private const int NonceBytes = 12; + private const int TagBytes = 16; + private const int KeyBytes = 32; + private const int Iterations = 100_000; + + private readonly string _passphrase; + + public SettingsProtector(BitweenOptions options) => _passphrase = options.SettingsEncryptionKey; + + /// Whether secrets can be stored at all. False = no passphrase configured. + public bool IsConfigured => !string.IsNullOrWhiteSpace(_passphrase); + + public string Protect(string plaintext) + { + if (!IsConfigured) + throw new InvalidOperationException( + $"{BitweenOptions.ConfigurationSection}:{nameof(BitweenOptions.SettingsEncryptionKey)} is not configured."); + + if (string.IsNullOrEmpty(plaintext)) return string.Empty; + + var salt = RandomNumberGenerator.GetBytes(SaltBytes); + var nonce = RandomNumberGenerator.GetBytes(NonceBytes); + var cipher = new byte[Encoding.UTF8.GetByteCount(plaintext)]; + var tag = new byte[TagBytes]; + + using var aes = new AesGcm(DeriveKey(salt), TagBytes); + aes.Encrypt(nonce, Encoding.UTF8.GetBytes(plaintext), cipher, tag); + + var payload = new byte[SaltBytes + NonceBytes + TagBytes + cipher.Length]; + salt.CopyTo(payload, 0); + nonce.CopyTo(payload, SaltBytes); + tag.CopyTo(payload, SaltBytes + NonceBytes); + cipher.CopyTo(payload, SaltBytes + NonceBytes + TagBytes); + + return Prefix + Convert.ToBase64String(payload); + } + + /// + /// Reverses . A value without the marker is returned untouched — that's + /// how a hand-written row, or one stored before a passphrase existed, still reads back. + /// Throws when the value was written under a different passphrase or has been altered. + /// + public string Unprotect(string stored) + { + if (string.IsNullOrEmpty(stored) || !stored.StartsWith(Prefix, StringComparison.Ordinal)) + return stored; + + var payload = Convert.FromBase64String(stored[Prefix.Length..]); + if (payload.Length < SaltBytes + NonceBytes + TagBytes) + throw new CryptographicException("Encrypted setting value is truncated."); + + var salt = payload.AsSpan(0, SaltBytes); + var nonce = payload.AsSpan(SaltBytes, NonceBytes); + var tag = payload.AsSpan(SaltBytes + NonceBytes, TagBytes); + var cipher = payload.AsSpan(SaltBytes + NonceBytes + TagBytes); + var plain = new byte[cipher.Length]; + + using var aes = new AesGcm(DeriveKey(salt.ToArray()), TagBytes); + aes.Decrypt(nonce, cipher, tag, plain); + + return Encoding.UTF8.GetString(plain); + } + + private byte[] DeriveKey(byte[] salt) => + Rfc2898DeriveBytes.Pbkdf2(_passphrase, salt, Iterations, HashAlgorithmName.SHA256, KeyBytes); +} diff --git a/SW.Bitween.Api/Services/Settings/SettingsService.cs b/SW.Bitween.Api/Services/Settings/SettingsService.cs new file mode 100644 index 00000000..d6b984be --- /dev/null +++ b/SW.Bitween.Api/Services/Settings/SettingsService.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; + +namespace SW.Bitween.Services; + +/// +/// Keeps the and singletons in sync with +/// the Settings table, which is the single source of truth for every editable setting. +/// +/// Configuration (env / appsettings) is read once per setting: the first boot after a key +/// exists copies what configuration says into a row (), and from then +/// on configuration is ignored for that key. "Default" therefore means the product default — +/// the initializer on the options class — which is what a reset returns a setting to. +/// +/// +/// Applying a value is just assigning the property: both option objects are singletons that every +/// consumer reads per call, so no consumer needs to know settings can change. +/// +/// +public class SettingsService +{ + /// Pristine options — never mutated, only read, to answer "what ships in the box?". + private static readonly SettingsTarget CodeDefaults = new(new BitweenOptions(), new ThemeOptions()); + + private readonly SettingsTarget _target; + private readonly SettingsProtector _protector; + private readonly ILogger _logger; + + /// + /// What configuration bound at startup. This is the import source, not the default — + /// captured before any stored value is applied, and used only for keys with no row yet. + /// + private readonly Dictionary _configured; + + public SettingsService(BitweenOptions bitweenOptions, ThemeOptions themeOptions, + SettingsProtector protector, ILogger logger) + { + _target = new SettingsTarget(bitweenOptions, themeOptions); + _protector = protector; + _logger = logger; + _configured = SettingsCatalog.All.ToDictionary(d => d.Key, d => d.Read(_target) ?? string.Empty); + } + + /// The product default: what this setting is without anyone having chosen anything. + public static string DefaultOf(SettingDefinition definition) => definition.Read(CodeDefaults) ?? string.Empty; + + /// + /// Product defaults for one key prefix, keyed the way the options object serializes + /// (Theme.LoginLogologinLogo). Lets an unauthenticated client tell a brand + /// value someone chose from one nobody has touched. + /// + public static Dictionary DefaultsUnder(string prefix) => SettingsCatalog.All + .Where(d => d.Key.StartsWith(prefix, StringComparison.Ordinal)) + .ToDictionary(d => Camelize(d.Key[prefix.Length..]), DefaultOf); + + /// + /// What this setting is right now on the live options singletons. For an environment-owned + /// setting that's the whole story — it's what configuration bound at startup. + /// + public string LiveValue(SettingDefinition definition) => definition.Read(_target) ?? string.Empty; + + /// A secret can only be stored if there's a passphrase to protect it with. + public bool CanStore(SettingDefinition definition) => + definition.Stored && (!definition.Secret || _protector.IsConfigured); + + /// Ciphertext for a secret, the value itself for everything else. + public string ToStored(SettingDefinition definition, string value) => + definition.Secret ? _protector.Protect(value) : value; + + /// + /// Copies configuration into a row for any key that doesn't have one yet — the one-time + /// hand-off that lets an existing deployment keep the values it was configured with. Runs at + /// startup only. + /// + public async Task ImportMissing(BitweenDbContext dbContext) + { + var stored = await dbContext.Set().AsNoTracking().Select(s => s.Id).ToListAsync(); + var have = new HashSet(stored, StringComparer.OrdinalIgnoreCase); + + var imported = new List(); + foreach (var definition in SettingsCatalog.All) + { + // Read-only and presence settings are environment-owned; they never get a row. + if (!definition.Stored) continue; + if (have.Contains(definition.Key)) continue; + // A secret with nowhere safe to go stays in configuration until a passphrase exists. + if (!CanStore(definition)) continue; + + var configured = _configured.GetValueOrDefault(definition.Key) ?? string.Empty; + dbContext.Add(new Setting { Id = definition.Key, Value = ToStored(definition, configured) }); + imported.Add(definition.Key); + } + + if (imported.Count == 0) return; + + try + { + await dbContext.SaveChangesAsync(); + _logger.LogInformation("Imported {Count} setting(s) from configuration: {Keys}", + imported.Count, string.Join(", ", imported)); + } + catch (DbUpdateException ex) + { + // Two instances booting together both try to import; whoever loses just reads the + // other's rows in Reload. Nothing to repair. + _logger.LogWarning(ex, "Setting import collided with another instance; using the stored rows."); + dbContext.ChangeTracker.Clear(); + } + } + + /// + /// Re-reads every stored setting and applies it. Called once before the app starts serving, + /// and again on each cache-revoke broadcast so an instance picks up a change made on another. + /// + public async Task Reload(BitweenDbContext dbContext) + { + var rows = await dbContext.Set().AsNoTracking() + .ToDictionaryAsync(s => s.Id, s => s.Value, StringComparer.OrdinalIgnoreCase); + + foreach (var definition in SettingsCatalog.All) + { + // Environment-owned settings stay that way even if a row is hand-inserted for one. + if (!definition.Stored) continue; + // No row means nobody could store this key yet (a secret with no passphrase), so + // whatever configuration bound at startup stands. + if (!rows.TryGetValue(definition.Key, out var stored)) continue; + + try + { + Assign(definition, definition.Secret ? _protector.Unprotect(stored) : stored); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Stored value for setting {Key} could not be read — it was likely written under a " + + "different {Option}. Leaving the running value untouched.", + definition.Key, nameof(BitweenOptions.SettingsEncryptionKey)); + } + } + } + + /// Applies one value immediately, so the request that saved it sees the effect. + public void Apply(SettingDefinition definition, string value) => Assign(definition, value); + + /// + /// Runs the real write against throwaway option objects, so the endpoint can reject "abc" + /// for a number before it reaches the database — without the live values ever seeing it. + /// + public static void Validate(SettingDefinition definition, string value) => + definition.Write(new SettingsTarget(new BitweenOptions(), new ThemeOptions()), value); + + /// + /// A bad stored value must never stop the app from booting: log it and leave the key at its + /// product default. + /// + private void Assign(SettingDefinition definition, string value) + { + try + { + definition.Write(_target, value); + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "Stored value for setting {Key} could not be applied; falling back to the product default.", + definition.Key); + try + { + definition.Write(_target, DefaultOf(definition)); + } + catch (Exception fallbackEx) + { + _logger.LogError(fallbackEx, "Product default for setting {Key} is itself invalid.", definition.Key); + } + } + } + + private static string Camelize(string name) => + name.Length == 0 ? name : char.ToLowerInvariant(name[0]) + name[1..]; +} diff --git a/SW.Bitween.Api/Services/ThemeOptions.cs b/SW.Bitween.Api/Services/ThemeOptions.cs index 84f3699d..ea2300f7 100644 --- a/SW.Bitween.Api/Services/ThemeOptions.cs +++ b/SW.Bitween.Api/Services/ThemeOptions.cs @@ -1,21 +1,37 @@ namespace SW.Bitween.Services { + /// + /// Branding. Every value here is an editable setting, so these initializers are the product + /// defaults — what "reset to default" returns a setting to. A deployment can still seed + /// different values through configuration, but only once: the first boot imports them into the + /// Settings table, and from then on the table is the only source (see SettingsService). + /// public class ThemeOptions { public const string ConfigurationSection = "Theme"; - public string LoginLogo { get; set; } - public string BitweenLogo { get; set; } - public string BitweenText { get; set; } - public string LinkedinLink { get; set; } - public string GithubLink { get; set; } - public string BitweenIcon { get; set; } - public string BitweenHeaderIcon { get; set; } - public string WebsiteLink { get; set; } - public string CompanyName { get; set; } - public string AllRightsReserved { get; set; } - public string CopyRightsIcon { get; set; } - public string TabTitle { get; set; } - public string TabIcon { get; set; } + public string LoginLogo { get; set; } = "/brand/BitweenFull.svg"; + public string BitweenLogo { get; set; } = "/brand/BitweenFull.svg"; + + public string BitweenText { get; set; } = + "is all-in-one solution to solving integration with third parties, automating workflows " + + "with exchanges coming from all forms of requests, ranging from internal messages to " + + "files dumped on a server."; + + public string LinkedinLink { get; set; } = "https://www.linkedin.com/company/simplify9"; + public string GithubLink { get; set; } = "https://github.com/simplify9"; + public string BitweenIcon { get; set; } = "/brand/BitweenIcon.png"; + public string BitweenHeaderIcon { get; set; } = "/brand/BitweenIcon.svg"; + public string WebsiteLink { get; set; } = "https://www.simplify9.com/"; + public string CompanyName { get; set; } = "Simplify9"; + public string AllRightsReserved { get; set; } = "All Rights Reserved."; + public string CopyRightsIcon { get; set; } = "©"; + public string TabTitle { get; set; } = "Bitween"; + public string TabIcon { get; set; } = "/favicon.svg"; + + /// Accent color the UI derives its whole brand ramp from. Hex, e.g. #e3311d. + public string PrimaryColor { get; set; } = "#e3311d"; + + public bool ShowFooter { get; set; } = true; } -} \ No newline at end of file +} diff --git a/SW.Bitween.Api/Services/XchangeService.cs b/SW.Bitween.Api/Services/XchangeService.cs index 31f6bb7f..3f7260ca 100644 --- a/SW.Bitween.Api/Services/XchangeService.cs +++ b/SW.Bitween.Api/Services/XchangeService.cs @@ -35,13 +35,15 @@ public class XchangeService : private readonly ILogger _logger; private readonly IInfolinkCache _BitweenCache; private readonly NativeAdapterDiscoveryService _nativeAdapterDiscovery; + private readonly AdapterInvoker _adapterInvoker; public XchangeService(BitweenOptions BitweenSettings, BitweenDbContext dbContext, FilterService filterService, ICloudFilesService cloudFiles, IServiceProvider serviceProvider, IPublish publish, ILogger logger, IInfolinkCache BitweenCache, - NativeAdapterDiscoveryService nativeAdapterDiscovery) + NativeAdapterDiscoveryService nativeAdapterDiscovery, AdapterInvoker adapterInvoker) { + _adapterInvoker = adapterInvoker; _BitweenSettings = BitweenSettings; _dbContext = dbContext; _filterService = filterService; @@ -85,17 +87,22 @@ public async Task SubmitFilterXchange(int documentId, XchangeFile file, string[] await _dbContext.SaveChangesAsync(); } - public async Task CreateXchange(Xchange xchange, XchangeFile file, WorkGroup workGroup, Dictionary groupAttemptCounts = null) + public async Task CreateXchange(Xchange xchange, XchangeFile file, WorkGroup workGroup, + bool manualRetry = false) { - var newXchange = new Xchange(xchange, file, workGroup, groupAttemptCounts); + var newXchange = new Xchange(xchange, file, workGroup, manualRetry); await AddFile(newXchange.Id, XchangeFileType.Input, file); _dbContext.Add(newXchange); } public async Task CreateXchange(Subscription subscription, Xchange xchange, XchangeFile file, - string[] references = null, Dictionary groupAttemptCounts = null) + string[] references = null, Dictionary groupAttemptCounts = null, bool manualRetry = false) { - var newXchange = new Xchange(subscription, xchange, file, groupAttemptCounts); + var partnerId = xchange.PartnerId ?? subscription.PartnerId; + var partner = partnerId.HasValue ? await _dbContext.FindAsync(partnerId.Value) : null; + var globalAdapterValuesSets = await _BitweenCache.ListGlobalAdapterValuesSetsAsync(); + var newXchange = new Xchange(subscription, xchange, file, partner, globalAdapterValuesSets, + groupAttemptCounts, manualRetry); await AddFile(newXchange.Id, XchangeFileType.Input, file); _dbContext.Add(newXchange); } @@ -114,6 +121,23 @@ public async Task CreateXchange(Subscription subscription, XchangeFile string[] references = null, string correlationId = null, Partner gatewayPartner = null, GlobalAdapterValuesSet[] globalAdapterValuesSets = null) { + // Callers that have no partner/globals context of their own (scheduled receivers, + // aggregation, manual "create exchange", plain internal subscription fan-out) leave + // this null — resolve it here so {{globals.…}} always gets a chance to translate, + // instead of silently no-op'ing for whichever caller forgot to load it. + globalAdapterValuesSets ??= await _BitweenCache.ListGlobalAdapterValuesSetsAsync(); + + // And the same for the partner, for the same reason. Only a caller that learned the + // partner from somewhere other than the subscription — a bus gateway route, a partner + // calling an API gateway — has one to hand in; everyone else left it null and the + // subscription's own partner went unused, so every {{partner.…}} in its adapters was + // written out literally and the handler ran against the template. This is the value + // the Xchange is attributed to either way (see PartnerId below), so filling from it + // adds a resolution that was missing rather than changing whose exchange it is. + gatewayPartner ??= subscription.PartnerId.HasValue + ? await _dbContext.FindAsync(subscription.PartnerId.Value) + : null; + var xchange = new Xchange(subscription, file, references, correlationId, gatewayPartner, globalAdapterValuesSets); await AddFile(xchange.Id, XchangeFileType.Input, file); @@ -141,17 +165,52 @@ public async Task ExecuteDelayedRetry(DelayedRetry delayedRetry) .FirstOrDefaultAsync(s => s.Id == xchange.SubscriptionId); if (subscription == null) { + // Recorded on the result like the unreadable-input case below, rather than only dropping + // the schedule: the exchange is still there for someone to look at, so leaving it with no + // reason means the retry simply stopped happening with nothing to explain it. + _dbContext.Remove(delayedRetry); + + var orphaned = await _dbContext.FindAsync(xchange.Id); + orphaned?.SetRetryBlocked( + "The scheduled retry was dropped: the subscription it belonged to no longer exists."); + return false; + } + + var inputFile = await ReadInputFile(xchange); + if (inputFile == null) + { + // The input is what a retry re-sends, so without it there is nothing to retry with. Handled + // like a missing subscription — drop the schedule and move on — but recorded on the result + // as well, because unlike a deleted subscription this needs someone to look into it. _dbContext.Remove(delayedRetry); + + var result = await _dbContext.FindAsync(xchange.Id); + result?.SetRetryBlocked("The scheduled retry was dropped: the input file could not be read."); return false; } - var inputFileData = await GetFile(xchange.Id, XchangeFileType.Input); - var inputFile = new XchangeFile(inputFileData, xchange.InputName); - await CreateXchange(subscription, xchange, inputFile, groupAttemptCounts: delayedRetry.GroupAttemptCounts); + await CreateXchange(subscription, xchange, inputFile); _dbContext.Remove(delayedRetry); return true; } + /// + /// The original input, or null when it cannot be read — deleted from storage, expired by a + /// lifecycle rule, or storage itself unavailable. + /// + private async Task ReadInputFile(Xchange xchange) + { + try + { + return new XchangeFile(await GetFile(xchange.Id, XchangeFileType.Input), xchange.InputName); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "The input file of xchange {XchangeId} could not be read.", xchange.Id); + return null; + } + } + private Task CreateOnHoldXchange(Subscription subscription, XchangeFile file, string[] references = null) { var xchange = new OnHoldXchange(subscription, file.Data, file.Filename, file.BadData, references); @@ -418,24 +477,101 @@ private async Task Process(XchangeMessage message) await CreateXchangesForHits(xchange, result, inputFile); } - _dbContext.Add(new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, responseXchange?.Id)); + var xchangeResult = new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, + responseXchange?.Id); + _dbContext.Add(xchangeResult); if (responseFile?.BadData == true) - await TryScheduleAutoRetry(xchange, XchangeResultType.BadResult, responseFile.Data); + await TrySchedulingWithoutLosingTheResult(xchange, XchangeResultType.BadResult, responseFile.Data, + xchangeResult); + else + await TryClearingRetryBudgetAfterSuccess(xchange); await _dbContext.SaveChangesAsync(); } catch (Exception ex) { - _dbContext.Add(new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, responseXchange?.Id, - ex.ToString())); - await TryScheduleAutoRetry(xchange, XchangeResultType.Error, ex.ToString()); + var xchangeResult = new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, + responseXchange?.Id, ex.ToString()); + _dbContext.Add(xchangeResult); + await TrySchedulingWithoutLosingTheResult(xchange, XchangeResultType.Error, ex.ToString(), xchangeResult); await _dbContext.SaveChangesAsync(); } } - private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resultType, string content) + /// + /// Evaluates the retry policy without ever costing the caller its failure record. + /// + /// + /// Scheduling runs before the is saved and touches the database + /// several times. Letting it throw would replace the original exception with its own and abort + /// the save, so the failure would vanish from the UI entirely and only reappear as a silent + /// redelivery. Losing the retry is recoverable; losing the record of what went wrong is not. + /// + private async Task TrySchedulingWithoutLosingTheResult(Xchange xchange, XchangeResultType resultType, + string content, XchangeResult xchangeResult) + { + try + { + await TryScheduleAutoRetry(xchange, resultType, content, xchangeResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Auto-retry evaluation failed for xchange {XchangeId}; the failure result is still recorded.", + xchange.Id); + } + } + + /// + /// Gives the subscription its retry budget back after a success, without ever costing the caller + /// its successful result. + /// + /// + /// Guarded for the same reason scheduling is, and with more at stake: this runs after the handler + /// has already delivered, so letting it throw would abort the save of a result whose side effects + /// have happened, and the redelivery would repeat them. A budget left spent is a nuisance somebody + /// can undo by hand; a duplicated delivery cannot be undone at all. + /// + private async Task TryClearingRetryBudgetAfterSuccess(Xchange xchange) + { + if (xchange.SubscriptionId == null) return; + + try + { + // The exchange's own start time is the watermark: anything charged after this run began + // belongs to a failure this success knows nothing about, and is left where it is. + await new RetryGroupBudget(_dbContext, _serviceProvider, xchange.SubscriptionId.Value) + .ReleaseExhaustedBudgets(xchange.StartedOn); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Retry budget of subscription {SubscriptionId} could not be cleared after a success; " + + "it may still refuse retries until it is reset.", xchange.SubscriptionId.Value); + } + } + + private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resultType, string content, + XchangeResult xchangeResult) { if (xchange.SubscriptionId == null) return; + // A person asked for this attempt, so the policy stays out of it. Otherwise pressing Retry + // spends a slot of the group's shared total — the budget meant for unattended retries — and + // can be what finally exhausts it and raises the alert. Recorded rather than skipped + // silently, so the absence of a follow-up attempt has a visible reason. + if (xchange.ManualRetry) + { + xchangeResult.SetRetryBlocked( + "This attempt was started by hand, so the retry policy left it alone and its budget is untouched."); + return; + } + + // DelayedRetry.Id is xchange.Id, so an existing row means this failure was already + // evaluated and already spent a slot of the group's total budget. Re-evaluating it + // (e.g. on an at-least-once redelivery) would both violate the PK on Add and spend a + // second slot for the same failure. + var alreadyScheduled = await _dbContext.Set().FindAsync(xchange.Id); + if (alreadyScheduled != null) return; + var subscription = await _dbContext.Set() .Include(s => s.RetryPolicy) .FirstOrDefaultAsync(s => s.Id == xchange.SubscriptionId.Value); @@ -443,35 +579,37 @@ private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resul IRetryPolicy policy = subscription?.CustomRetryPolicy ?? (IRetryPolicy)subscription?.RetryPolicy; if (policy?.Groups == null || policy.Groups.Count == 0) return; - var evaluator = new RetryPolicyEvaluator(policy); - evaluator.RestoreGroupAttemptCounts(xchange.GroupAttemptCounts == null - ? new Dictionary() - : new Dictionary(xchange.GroupAttemptCounts)); + var evaluator = new RetryPolicyEvaluator(policy, + new RetryGroupBudget(_dbContext, _serviceProvider, xchange.SubscriptionId.Value)); var attemptIndex = await CountRetryChainDepth(xchange); - var decision = evaluator.Evaluate(resultType, content, attemptIndex); + var decision = await evaluator.Evaluate(resultType, content, attemptIndex); + + // Which group owned this failure, so the group's retries can later be listed without + // re-deriving the match, and how deep the chain already was without walking it again. + if (decision.MatchedGroup is not null) + xchangeResult.SetRetryEvaluation(decision.MatchedGroup.Id, attemptIndex); if (decision.ShouldRetry) - { - // Guard against duplicate scheduling (e.g. an at-least-once redelivery - // reprocessing the same xchange) — DelayedRetry.Id is xchange.Id, so a - // blind Add would violate the PK and fail the whole SaveChangesAsync. - var existing = await _dbContext.Set().FindAsync(xchange.Id); - if (existing != null) + _dbContext.Add(new DelayedRetry { - existing.On = DateTime.UtcNow + decision.Delay; - existing.GroupAttemptCounts = evaluator.GetGroupAttemptCounts(); - } - else - { - _dbContext.Add(new DelayedRetry - { - Id = xchange.Id, - On = DateTime.UtcNow + decision.Delay, - GroupAttemptCounts = evaluator.GetGroupAttemptCounts() - }); - } - } + Id = xchange.Id, + On = DateTime.UtcNow + decision.Delay + }); + else + // A policy applied but refused. Recorded so an exhausted budget is distinguishable + // from an error no group was ever configured to catch. + xchangeResult.SetRetryBlocked(decision.Reason); + + // Raised on the result rather than published here, so the alert only reaches the bus once + // this failure is committed. Its own event type means its own queue and its own consumer, + // keeping a slow alert handler away from the ordinary notifier path. + if (decision.BudgetJustExhausted) + xchangeResult.RaiseBudgetExhausted( + xchange.SubscriptionId.Value, + decision.MatchedGroup!.Id, + decision.MatchedGroup.Name, + decision.MatchedGroup.Budget!.MaxAttemptsTotal); } private async Task CountRetryChainDepth(Xchange xchange) @@ -606,20 +744,8 @@ private async Task NotifyResult(Notifier notifier, XchangeResult xchangeResult, try { - // Check if it's a native adapter - if (notifier.HandlerId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) - { - var handler = _nativeAdapterDiscovery.GetNativeHandler(notifier.HandlerId, handlerProperties); - await handler.Handle(new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); - } - else - { - // Use serverless for external adapters - var serverless = _serviceProvider.GetRequiredService(); - await serverless.StartAsync(notifier.HandlerId, correlationId, handlerProperties); - await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), - new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); - } + await _adapterInvoker.Handle(notifier.HandlerId, handlerProperties, correlationId, + new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); _dbContext.Add(new XchangeNotification(xchangeResult.Id, notifier.Id, notifier.Name)); } diff --git a/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs b/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs new file mode 100644 index 00000000..489b470a --- /dev/null +++ b/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.IntegrationTests.Adapters; + +/// Always finds nothing — for exercising ReceivingJob's no-new-data path. +public class NativeEmptyTestReceiver : INativeInfolinkReceiver +{ + public string Name => nameof(NativeEmptyTestReceiver); + public Type StartupValuesType => typeof(object); + + public void InitializeStartupValues(IDictionary settings) { } + + public Task Initialize() => Task.CompletedTask; + + public Task> ListFiles() => Task.FromResult>(Array.Empty()); + + public Task GetFile(string fileId) => throw new NotSupportedException(); + + public Task DeleteFile(string fileId) => Task.CompletedTask; + + public Task Finalize() => Task.CompletedTask; +} diff --git a/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs b/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs new file mode 100644 index 00000000..1a4d1d51 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.IntegrationTests.Adapters; + +/// Always fails to list files — for exercising ReceivingJob's failure path. +public class NativeFailingTestReceiver : INativeInfolinkReceiver +{ + public string Name => nameof(NativeFailingTestReceiver); + public Type StartupValuesType => typeof(object); + + public void InitializeStartupValues(IDictionary settings) { } + + public Task Initialize() => Task.CompletedTask; + + public Task> ListFiles() => throw new InvalidOperationException("Connection refused"); + + public Task GetFile(string fileId) => throw new NotSupportedException(); + + public Task DeleteFile(string fileId) => Task.CompletedTask; + + public Task Finalize() => Task.CompletedTask; +} diff --git a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs index 33824ad2..c4274999 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs @@ -11,12 +11,15 @@ using SW.Bitween.Domain; using SW.Bitween.IntegrationTests.Adapters; using SW.Bitween.NativeAdapters; +using SW.Bitween.NativeAdapters.SmtpHandler; using SW.Bitween.PgSql; using SW.Bus; using SW.CloudFiles.Extensions; using SW.CloudFiles.LocalTests; using SW.PrimitiveTypes; using SW.Serverless; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; using Testcontainers.PostgreSql; using Testcontainers.RabbitMq; using Xunit; @@ -24,15 +27,35 @@ namespace SW.Bitween.IntegrationTests.Fixtures; /// -/// Collection-scoped fixture that starts a PostgreSQL container and a RabbitMQ container, -/// applies EF migrations, installs serverless adapters to local cloud storage, and builds -/// a fully wired service provider. +/// Collection-scoped fixture that starts a PostgreSQL container, a RabbitMQ container and a +/// MailHog container, applies EF migrations, installs serverless adapters to local cloud storage, +/// and builds a fully wired service provider. /// public sealed class BitweenFixture : IAsyncLifetime { private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder().Build(); private readonly RabbitMqContainer _rabbitMq = new RabbitMqBuilder().Build(); + // A real SMTP server, because the one thing no unit test can prove about the alert feature is + // that an actual handshake succeeds. Started here rather than expected on the developer's + // machine: a test that quietly does nothing when a local service is missing reports a green run + // while the whole delivery path goes unexercised. + private readonly IContainer _mailHog = new ContainerBuilder() + .WithImage("mailhog/mailhog:v1.0.1") + .WithPortBinding(SmtpContainerPort, true) + .WithPortBinding(ApiContainerPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(ApiContainerPort)) + .Build(); + + private const int SmtpContainerPort = 1025; + private const int ApiContainerPort = 8025; + + /// Host port the MailHog SMTP listener is mapped to, for a handler's Port setting. + public int MailHogSmtpPort => _mailHog.GetMappedPublicPort(SmtpContainerPort); + + /// Base address of MailHog's own API, for reading back what was delivered. + public string MailHogApi => $"http://{_mailHog.Hostname}:{_mailHog.GetMappedPublicPort(ApiContainerPort)}"; + public IHost App { get; private set; } = null!; private ExceptionDispatchInfo? _initError; @@ -41,7 +64,7 @@ public async Task InitializeAsync() { try { - await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync()); + await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.StartAsync()); var dataSourceBuilder = new NpgsqlDataSourceBuilder(_postgres.GetConnectionString()); dataSourceBuilder.EnableDynamicJson(); @@ -92,14 +115,22 @@ public async Task InitializeAsync() services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); }) .Build(); @@ -143,6 +174,7 @@ public async Task DisposeAsync() } await _postgres.DisposeAsync(); await _rabbitMq.DisposeAsync(); + await _mailHog.DisposeAsync(); } } diff --git a/SW.Bitween.IntegrationTests/Fixtures/TestRequestContext.cs b/SW.Bitween.IntegrationTests/Fixtures/TestRequestContext.cs new file mode 100644 index 00000000..a788ed4f --- /dev/null +++ b/SW.Bitween.IntegrationTests/Fixtures/TestRequestContext.cs @@ -0,0 +1,21 @@ +using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; +using SW.PrimitiveTypes; + +namespace SW.Bitween.IntegrationTests.Fixtures; + +internal static class TestRequestContext +{ + /// + /// Handlers resolve their permissions from the database, and no account is signed in during + /// these tests. The break-glass superuser claim grants the whole catalog, so a test exercises + /// the handler rather than the guard in front of it. + /// + public static RequestContext Superuser(this AsyncServiceScope scope) + { + var ctx = scope.ServiceProvider.GetRequiredService(); + ctx.Set(new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(Bitween.RequestContextExtensions.SuperuserClaim, "true")], "integration-test"))); + return ctx; + } +} diff --git a/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj b/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj index 72599a1e..53c76358 100644 --- a/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj +++ b/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable false SW.Bitween.IntegrationTests @@ -33,7 +33,7 @@ - + diff --git a/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs b/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs index a410e084..1c3657b2 100644 --- a/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs @@ -121,7 +121,7 @@ public async Task DelayedRetries_Search_returns_expected_row() db.Set().Add(new DelayedRetry { Id = xchange.Id, On = scheduledOn }); await db.SaveChangesAsync(); - var search = new SW.Bitween.Resources.DelayedRetries.Search(db); + var search = new SW.Bitween.Resources.DelayedRetries.Search(db, scope.Superuser()); var response = (SearchyResponse)await search.Handle(EmptySearch()); var row = response.Result.FirstOrDefault(r => r.Id == xchange.Id); @@ -141,7 +141,7 @@ public async Task RunNow_executes_immediately_even_when_not_yet_due_and_removes_ await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var xs = scope.ServiceProvider.GetRequiredService(); - var ctx = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); var (_, _, xchange) = await CreateSubscriptionWithXchange(db, xs, 9006, "Run Now Doc"); // Scheduled an hour from now — RunNow must still execute it immediately. @@ -164,7 +164,7 @@ public async Task RunNow_throws_when_nothing_is_scheduled() await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var xs = scope.ServiceProvider.GetRequiredService(); - var ctx = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); var runNow = new SW.Bitween.Resources.DelayedRetries.RunNow(db, ctx, xs); @@ -186,7 +186,7 @@ public async Task Xchanges_Search_includes_ScheduledRetryOn_when_delayed_retry_e db.Set().Add(new DelayedRetry { Id = xchange.Id, On = scheduledOn }); await db.SaveChangesAsync(); - var search = new SW.Bitween.Resources.Xchanges.Search(db, xs); + var search = new SW.Bitween.Resources.Xchanges.Search(db, xs, scope.Superuser()); var response = (SearchyResponse)await search.Handle(EmptySearch()); var row = response.Result.FirstOrDefault(r => r.Id == xchange.Id); @@ -203,7 +203,7 @@ public async Task Xchanges_Search_has_null_ScheduledRetryOn_when_no_delayed_retr var xs = scope.ServiceProvider.GetRequiredService(); var (_, _, xchange) = await CreateSubscriptionWithXchange(db, xs, 9008, "Xchange Search Unscheduled Doc"); - var search = new SW.Bitween.Resources.Xchanges.Search(db, xs); + var search = new SW.Bitween.Resources.Xchanges.Search(db, xs, scope.Superuser()); var response = (SearchyResponse)await search.Handle(EmptySearch()); var row = response.Result.FirstOrDefault(r => r.Id == xchange.Id); diff --git a/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs b/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs new file mode 100644 index 00000000..5bb86af1 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// {{partner.…}} in an adapter's properties has to be replaced by the time the Xchange is +/// written, because that is the copy the handler runs on — nothing resolves it later. +/// +/// +/// Every caller of CreateXchange that has a partner in hand (a bus gateway route, an API +/// gateway call) passed one, and every caller that didn't left the token literal — so a handler +/// posted to the URL {{partner.webhookUrl}}. The subscription's own partner was sitting +/// on the subscription the whole time; these tests pin down that it is now used. +/// +[Collection("Bitween")] +public class PartnerTokenTests +{ + private readonly BitweenFixture _fixture; + + public PartnerTokenTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Subscription_own_partner_fills_handler_tokens() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xchangeService = scope.ServiceProvider.GetRequiredService(); + + var partner = new Partner("Token Partner") + { + AdapterProperties = new Dictionary { ["merchantSlug"] = "acme" } + }; + db.Set().Add(partner); + await db.SaveChangesAsync(); + + var doc = new Document(6101, "Partner Token Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // Internal: the type whose only trigger is a document of its type arriving, so it reaches + // CreateXchange without a partner being handed in from outside. + var sub = new Subscription("Token Sub", doc.Id, SubscriptionType.Internal, partner.Id); + sub.Inactive = false; + sub.SetDictionaries( + new Dictionary { ["Url"] = "http://host/{{partner.merchantSlug}}" }, + new Dictionary(), + new Dictionary(), + new Dictionary(), + new Dictionary()); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var xchange = await xchangeService.CreateXchange(sub, new XchangeFile("{\"id\":1}")); + await db.SaveChangesAsync(); + + Assert.Equal("http://host/acme", xchange.HandlerProperties["Url"]); + Assert.Equal(partner.Id, xchange.PartnerId); + } + + [Fact] + public async Task Partner_handed_in_wins_over_the_subscriptions_own() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xchangeService = scope.ServiceProvider.GetRequiredService(); + + var own = new Partner("Own Partner") + { + AdapterProperties = new Dictionary { ["merchantSlug"] = "own" } + }; + var routed = new Partner("Routed Partner") + { + AdapterProperties = new Dictionary { ["merchantSlug"] = "routed" } + }; + db.Set().AddRange(own, routed); + await db.SaveChangesAsync(); + + var doc = new Document(6102, "Partner Token Doc 2"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription("Token Sub 2", doc.Id, SubscriptionType.Internal, own.Id); + sub.Inactive = false; + sub.SetDictionaries( + new Dictionary { ["Url"] = "http://host/{{partner.merchantSlug}}" }, + new Dictionary(), + new Dictionary(), + new Dictionary(), + new Dictionary()); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + // A bus gateway route's partner: the caller knows better than the subscription does, + // so the fallback must not override it. + var xchange = await xchangeService.CreateXchange(sub, new XchangeFile("{\"id\":2}"), + gatewayPartner: routed); + await db.SaveChangesAsync(); + + Assert.Equal("http://host/routed", xchange.HandlerProperties["Url"]); + Assert.Equal(routed.Id, xchange.PartnerId); + } + + [Fact] + public async Task No_partner_anywhere_leaves_the_token_alone() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xchangeService = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(6103, "Partner Token Doc 3"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // A bus gateway subscription carries no partner of its own; a route may or may not + // supply one. With none, there is nothing to resolve against and the token stands. + var sub = new Subscription("Token Sub 3", doc.Id, SubscriptionType.BusGateway); + sub.Inactive = false; + sub.SetDictionaries( + new Dictionary { ["Url"] = "http://host/{{partner.merchantSlug}}" }, + new Dictionary(), + new Dictionary(), + new Dictionary(), + new Dictionary()); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var xchange = await xchangeService.CreateXchange(sub, new XchangeFile("{\"id\":3}")); + await db.SaveChangesAsync(); + + Assert.Equal("http://host/{{partner.merchantSlug}}", xchange.HandlerProperties["Url"]); + Assert.Null(xchange.PartnerId); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs b/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs index 7fdbeb18..ef6900bb 100644 --- a/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -47,6 +49,98 @@ public async Task Receiving_job_creates_one_xchange_per_received_file() Assert.Equal(2, count); } + [Fact] + public async Task Receiving_job_records_one_attempt_with_the_exchanges_it_created() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + var document = new Document(6006, "Receiving Attempt Doc"); + db.Set().Add(document); + + var subscription = new Subscription("Receive Attempt Test", document.Id); + subscription.ReceiverId = nameof(NativeTestReceiver); + subscription.Inactive = false; + // SetSchedules() throws "Invalid schedule" with none configured — a real Receiving + // subscription always has one, so give this test one too. + subscription.SetSchedules(new[] { new Schedule(Recurrence.Hourly, TimeSpan.FromMinutes(30)) }); + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + cache.Revoke(); + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + var attempt = await db.Set().SingleAsync(a => a.SubscriptionId == subscription.Id); + Assert.Equal(ReceiveOutcome.Received, attempt.Outcome); + Assert.Null(attempt.ErrorMessage); + Assert.Equal(2, attempt.ExchangeIds.Length); + + var xchangeIds = await db.Set() + .Where(x => x.SubscriptionId == subscription.Id) + .Select(x => x.Id) + .ToListAsync(); + Assert.Equal(xchangeIds.OrderBy(i => i), attempt.ExchangeIds.OrderBy(i => i)); + } + + [Fact] + public async Task Receiving_job_records_a_failed_attempt_when_listing_files_throws() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + var document = new Document(6007, "Receiving Failure Doc"); + db.Set().Add(document); + + var subscription = new Subscription("Receive Failure Test", document.Id); + subscription.ReceiverId = nameof(NativeFailingTestReceiver); + subscription.Inactive = false; + subscription.SetSchedules(new[] { new Schedule(Recurrence.Hourly, TimeSpan.FromMinutes(30)) }); + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + cache.Revoke(); + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + var attempt = await db.Set().SingleAsync(a => a.SubscriptionId == subscription.Id); + Assert.Equal(ReceiveOutcome.Failed, attempt.Outcome); + Assert.Contains("Connection refused", attempt.ErrorMessage); + Assert.Empty(attempt.ExchangeIds); + } + + [Fact] + public async Task Receiving_job_records_no_new_data_when_nothing_is_found() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + var document = new Document(6008, "Receiving Empty Doc"); + db.Set().Add(document); + + var subscription = new Subscription("Receive Empty Test", document.Id); + subscription.ReceiverId = nameof(NativeEmptyTestReceiver); + subscription.Inactive = false; + subscription.SetSchedules(new[] { new Schedule(Recurrence.Hourly, TimeSpan.FromMinutes(30)) }); + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + cache.Revoke(); + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + var attempt = await db.Set().SingleAsync(a => a.SubscriptionId == subscription.Id); + Assert.Equal(ReceiveOutcome.NoNewData, attempt.Outcome); + Assert.Null(attempt.ErrorMessage); + Assert.Empty(attempt.ExchangeIds); + } + [Fact] public async Task Receiving_job_does_nothing_for_inactive_subscription() { diff --git a/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs new file mode 100644 index 00000000..5e445634 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Sends a real "retry budget exhausted" alert through to a local +/// MailHog instance and reads it back over MailHog's own API — the one part of the feature no unit +/// test can prove, because it depends on an actual SMTP handshake succeeding. +/// +/// +/// MailHog is started by alongside PostgreSQL and RabbitMQ, so these +/// tests run everywhere the rest of the suite does. They used to return early when a local MailHog +/// was missing, which xunit reports as a pass — a green run then said nothing about whether an alert +/// can actually be delivered. +/// +[Collection("Bitween")] +public class RetryAlertServiceTests +{ + // MailHog answers instantly or not at all, so the default 100 seconds only ever means "the run + // hangs instead of failing". + private static readonly TimeSpan MailHogTimeout = TimeSpan.FromSeconds(5); + + private readonly BitweenFixture _fixture; + + private string MessagesApi => $"{_fixture.MailHogApi}/api/v2/messages"; + + public RetryAlertServiceTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + // Deleting is only exposed on MailHog's v1 API — the v2 route 404s and would silently leave + // messages behind, making the assertions depend on leftovers from the previous run. + private async Task ClearMailHog() + { + using var http = new HttpClient { Timeout = MailHogTimeout }; + var response = await http.DeleteAsync($"{_fixture.MailHogApi}/api/v1/messages"); + response.EnsureSuccessStatusCode(); + } + + private async Task LatestMailHogMessage() + { + using var http = new HttpClient { Timeout = MailHogTimeout }; + var json = await http.GetStringAsync(MessagesApi); + using var doc = JsonDocument.Parse(json); + var items = doc.RootElement.GetProperty("items").Clone(); + return items.GetArrayLength() > 0 ? items[0] : null; + } + + private async Task MailHogTotal() + { + using var http = new HttpClient { Timeout = MailHogTimeout }; + var json = await http.GetStringAsync(MessagesApi); + using var doc = JsonDocument.Parse(json); + return doc.RootElement.GetProperty("total").GetInt32(); + } + + [Fact] + public async Task Exhausted_budget_alert_arrives_in_MailHog_with_the_group_and_subscription_named() + { + await ClearMailHog(); + + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var alertService = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7201, "MailHog Alert Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // A group whose own alert config points at MailHog directly — the narrowest level, so the + // resolver has nothing to fall through to and the test proves that level specifically. + var groupId = Guid.NewGuid(); + var policy = new RetryPolicy + { + Name = "MailHog Alert Policy", + Groups = + [ + new RetryGroup + { + Id = groupId, + Name = "FRT charges cannot be found", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 1, + MaxAttemptsTotal = 1, + DelayStrategy = new FixedDelayStrategy { DelayMs = 1000 } + }, + AlertMode = RetryAlertMode.Send, + AlertHandlerId = "NativeSmtpHandler", + AlertHandlerProperties = new Dictionary + { + ["Host"] = "localhost", + ["Port"] = _fixture.MailHogSmtpPort.ToString(), + ["UseTls"] = "false", + ["From"] = "bitween-alerts@example.com", + ["To"] = "ops@example.com", + ["Subject"] = "Retries stopped for {{ SubscriptionName }}", + ["Body"] = "{{ GroupName }} used all {{ MaxAttemptsTotal }} retries." + } + } + ] + }; + db.Set().Add(policy); + await db.SaveChangesAsync(); + + var sub = new Subscription("MailHog Alert Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policy.Id, null); + await db.SaveChangesAsync(); + + var xchange = await scope.ServiceProvider.GetRequiredService() + .CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + // Reproduces exactly what TryScheduleAutoRetry does: evaluate against the real budget table, + // and when it reports exhaustion, raise the event the same way XchangeResult does in + // production. RetryAlertService.Process is then invoked directly rather than over the bus — + // this suite calls handlers directly throughout (see RetryJobTests, DelayedRetriesTests) + // rather than relying on live message transport, which is SW.Bus's own concern, not this + // feature's. + var evaluator = new RetryPolicyEvaluator(policy, + new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)); + + // Total is 1, so the first message (a different "parcel" failing the same way) spends the + // whole budget and is itself allowed to retry — exhaustion only shows up for the next one. + var firstMessage = await evaluator.Evaluate(XchangeResultType.Error, + "System.TimeoutException: contains timeout", 0); + Assert.True(firstMessage.ShouldRetry); + + var decision = await evaluator.Evaluate(XchangeResultType.Error, + "System.TimeoutException: contains timeout", 0); + Assert.False(decision.ShouldRetry); + Assert.True(decision.BudgetJustExhausted); + + var xchangeResult = new XchangeResult(xchange.Id, null, null, exception: "System.TimeoutException: contains timeout"); + xchangeResult.RaiseBudgetExhausted(sub.Id, groupId, decision.MatchedGroup!.Name, + decision.MatchedGroup.Budget!.MaxAttemptsTotal); + + // SaveChangesAsync dispatches and clears Events (see BitweenDbContext), same as it does in + // production, so the event has to be captured before saving rather than read back after. + // The constructor raises its own XchangeResultCreatedEvent alongside it. + var raisedEvent = Assert.Single(xchangeResult.Events.OfType()); + + db.Add(xchangeResult); + await db.SaveChangesAsync(); + + await alertService.Process(raisedEvent); + + var message = await LatestMailHogMessage(); + Assert.NotNull(message); + + var subject = message!.Value.GetProperty("Content").GetProperty("Headers") + .GetProperty("Subject")[0].GetString(); + Assert.Equal("Retries stopped for MailHog Alert Sub", subject); + + var body = message.Value.GetProperty("Content").GetProperty("Body").GetString(); + Assert.Contains("FRT charges cannot be found used all 1 retries", body); + + var loggedNotification = await db.Set().AsNoTracking() + .SingleAsync(n => n.XchangeId == xchange.Id); + Assert.True(loggedNotification.Success); + Assert.Equal(XchangeNotification.RetryBudgetAlertName, loggedNotification.NotifierName); + + // Redelivery of the same event must not double-send — same guard the real bus retry path + // relies on. Compared against the count after the first send rather than an absolute + // number, so a stray message could never make this pass by accident. + var totalAfterFirstSend = await MailHogTotal(); + await alertService.Process(raisedEvent); + Assert.Equal(totalAfterFirstSend, await MailHogTotal()); + } + + [Fact] + public async Task A_failed_send_does_not_stop_a_later_delivery() + { + await ClearMailHog(); + + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var alertService = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7202, "Failed Send Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + var policy = new RetryPolicy + { + Name = "Failed Send Policy", + Groups = + [ + new RetryGroup + { + Id = groupId, + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 1, + MaxAttemptsTotal = 1, + DelayStrategy = new FixedDelayStrategy { DelayMs = 1000 } + }, + AlertMode = RetryAlertMode.Send, + AlertHandlerId = "NativeSmtpHandler", + AlertHandlerProperties = new Dictionary + { + ["Host"] = "localhost", + ["Port"] = _fixture.MailHogSmtpPort.ToString(), + ["UseTls"] = "false", + ["From"] = "bitween-alerts@example.com", + ["To"] = "ops@example.com", + ["Subject"] = "Retries stopped for {{ SubscriptionName }}", + ["Body"] = "{{ GroupName }} used all {{ MaxAttemptsTotal }} retries." + } + } + ] + }; + db.Set().Add(policy); + await db.SaveChangesAsync(); + + var sub = new Subscription("Failed Send Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policy.Id, null); + await db.SaveChangesAsync(); + + var xchange = await scope.ServiceProvider.GetRequiredService() + .CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + // Stands in for a first attempt that threw — a dropped connection, a refused relay. Written + // directly because what matters is the row it leaves behind, not how the send failed. + db.Add(XchangeNotification.ForRetryBudgetAlert(xchange.Id, "System.Net.Sockets.SocketException: refused")); + await db.SaveChangesAsync(); + + var xchangeResult = new XchangeResult(xchange.Id, null, null, exception: "timeout"); + xchangeResult.RaiseBudgetExhausted(sub.Id, groupId, "Timeout", 1); + var raisedEvent = Assert.Single(xchangeResult.Events.OfType()); + db.Add(xchangeResult); + await db.SaveChangesAsync(); + + // The recoverable failure must not read as "already delivered": a transient error would + // otherwise silence the alert for good, which is the opposite of what a retry system owes. + await alertService.Process(raisedEvent); + + Assert.Equal(1, await MailHogTotal()); + Assert.True(await db.Set() + .AnyAsync(n => n.XchangeId == xchange.Id + && n.NotifierName == XchangeNotification.RetryBudgetAlertName + && n.Success)); + + // And now that one did get through, the guard has to hold: no third row, no second email. + // Both are asserted — a redelivery that wrongly logged another success while sending nothing + // would otherwise pass here. + var rowsAfterDelivery = await db.Set() + .CountAsync(n => n.XchangeId == xchange.Id); + + await alertService.Process(raisedEvent); + + Assert.Equal(1, await MailHogTotal()); + Assert.Equal(rowsAfterDelivery, + await db.Set().CountAsync(n => n.XchangeId == xchange.Id)); + } + + [Fact] + public async Task The_handler_refuses_to_send_a_password_over_an_unencrypted_connection() + { + await ClearMailHog(); + + await using var scope = _fixture.CreateScope(); + var discovery = scope.ServiceProvider.GetRequiredService(); + + // MailHog speaks plain SMTP on 1025, which is exactly the shape of the mistake worth + // catching: a working relay, no encryption, and a password to hand over. + var handler = discovery.GetNativeHandler("NativeSmtpHandler", new Dictionary + { + ["Host"] = "localhost", + ["Port"] = _fixture.MailHogSmtpPort.ToString(), + ["UseTls"] = "false", + ["Password"] = "hunter2", + ["From"] = "bitween-alerts@example.com", + ["To"] = "ops@example.com", + ["Subject"] = "Should never be sent", + ["Body"] = "Should never be sent" + }); + + // Matched on the message, not just the type: the handler also throws + // InvalidOperationException for a missing recipient, so dropping the To above would otherwise + // leave this passing without ever reaching the credential guard. + var refusal = await Assert.ThrowsAsync( + () => handler.Handle(new XchangeFile("{}"))); + Assert.Contains("will not send a password over an unencrypted connection", refusal.Message); + + // Refusing has to mean refusing: no message, and therefore no password, left the process. + Assert.Equal(0, await MailHogTotal()); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs index 5bf58c4d..e150a9b4 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using SW.Bitween.Domain; using SW.Bitween.IntegrationTests.Fixtures; using SW.Bitween.Model; @@ -24,7 +25,7 @@ public RetryJobTests(BitweenFixture fixture) // ─── Helpers ────────────────────────────────────────────────────────────── private RetryJob BuildJob(BitweenDbContext db, XchangeService xchangeService) => - new(db, xchangeService); + new(db, xchangeService, NullLogger.Instance); // ─── Batch query ────────────────────────────────────────────────────────── @@ -101,93 +102,162 @@ public async Task RetryJob_removes_delayed_retry_when_subscription_is_missing() "A DelayedRetry whose Subscription no longer exists must be removed without creating a retry Xchange."); } - // ─── Full execution path ────────────────────────────────────────────────── + // ─── One bad row must not stop the rest ─────────────────────────────────── + + /// + /// An Xchange whose input file was never uploaded: reading it fails, which is what a retry whose + /// file has since been deleted from storage looks like. + /// + private static async Task AddUnreadableXchange(BitweenDbContext db, Subscription sub) + { + var xchange = new Xchange(sub, new XchangeFile("{}")); + db.Set().Add(xchange); + db.Set().Add(new XchangeResult(xchange.Id, null, null, exception: "boom")); + await db.SaveChangesAsync(); + return xchange; + } [Fact] - public async Task RetryJob_processes_due_delayed_retry_and_creates_retry_xchange() + public async Task RetryJob_drops_a_retry_whose_input_is_gone_and_still_runs_the_others() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var xs = scope.ServiceProvider.GetRequiredService(); - var doc = new Document(8001, "RetryJob Due Doc"); + var doc = new Document(8010, "RetryJob Missing Input Doc"); db.Set().Add(doc); await db.SaveChangesAsync(); - var sub = new Subscription("RetryJob Sub", doc.Id); - sub.Inactive = false; + var sub = new Subscription("RetryJob Missing Input Sub", doc.Id) { Inactive = false }; db.Set().Add(sub); await db.SaveChangesAsync(); - // Use CreateXchange so the input file is uploaded to real cloud storage - var originalXchange = await xs.CreateXchange(sub, new XchangeFile("{}")); + var unreadable = await AddUnreadableXchange(db, sub); + var healthy = await xs.CreateXchange(sub, new XchangeFile("{}")); - var groupCounts = new System.Collections.Generic.Dictionary - { - [Guid.NewGuid().ToString()] = 1 - }; - var delayedRetry = new DelayedRetry + db.Set().AddRange( + new DelayedRetry { Id = unreadable.Id, On = DateTime.UtcNow.AddMinutes(-2) }, + new DelayedRetry { Id = healthy.Id, On = DateTime.UtcNow.AddMinutes(-1) }); + await db.SaveChangesAsync(); + + await BuildJob(db, xs).Execute(); + + // With one commit per row, the retry that could not be made does not undo the one that could. + // Committing the batch in one go would have lost both and left both schedules behind. + Assert.False(await db.Set().AnyAsync(r => r.Id == healthy.Id)); + Assert.True(await db.Set().AnyAsync(x => x.RetryFor == healthy.Id)); + + // The unusable one leaves the queue too, rather than being tried again every minute for good. + Assert.False(await db.Set().AnyAsync(r => r.Id == unreadable.Id)); + + // And it says so where a reader already looks for "why is this not being retried?". + var result = await db.Set().AsNoTracking().SingleAsync(r => r.Id == unreadable.Id); + Assert.Contains("input file could not be read", result.RetryBlockedReason); + } + + [Fact] + public async Task RetryJob_works_through_more_than_one_batch_in_a_single_run() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + // Schedules pointing at exchanges that no longer exist: the cheapest row to process, and enough + // of them to need more than one batch of 100. + var ids = Enumerable.Range(0, 105) + .Select(i => $"rjt-batch-{Guid.NewGuid():N}-{i}") + .ToList(); + + db.Set().AddRange(ids.Select((id, i) => new DelayedRetry { - Id = originalXchange.Id, - On = DateTime.UtcNow.AddMinutes(-1), - GroupAttemptCounts = groupCounts - }; - db.Set().Add(delayedRetry); + Id = id, + On = DateTime.UtcNow.AddMinutes(-(i + 1)) + })); await db.SaveChangesAsync(); await BuildJob(db, xs).Execute(); - // DelayedRetry must be gone - var retryGone = !await db.Set().AnyAsync(r => r.Id == originalXchange.Id); - Assert.True(retryGone, "The processed DelayedRetry record must be deleted."); + // All of them, not the first hundred: a backlog should not have to wait a minute per hundred. + var left = await db.Set().CountAsync(r => ids.Contains(r.Id)); + Assert.Equal(0, left); + } - // A new Xchange with RetryFor pointing to the original must exist - var retryXchange = await db.Set() - .FirstOrDefaultAsync(x => x.RetryFor == originalXchange.Id); - Assert.NotNull(retryXchange); - Assert.Equal(originalXchange.Id, retryXchange.RetryFor); - Assert.Equal(sub.Id, retryXchange.SubscriptionId); + // ─── Bulk retry with no subscription ────────────────────────────────────── + + [Fact] + public async Task BulkRetry_handles_an_exchange_with_no_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(8012, "BulkRetry No Sub Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // A document-only exchange: SubscriptionId is null from the start, which is also what an + // exchange whose subscription was later deleted looks like. Created through the service so its + // input file is really in storage, since bulk retry reads it before looking anything else up. + var orphan = await xs.CreateXchange(doc, WorkGroup.None, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + var healthy = await xs.CreateXchange(doc, WorkGroup.None, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + // One selection containing both. This threw before, so the whole bulk retry failed — including + // for the exchanges that were perfectly retryable. + await new Resources.Xchanges.BulkRetry(db, xs).Handle(new XchangeBulkRetry + { + Ids = [orphan.Id, healthy.Id], + Reset = false + }); + await db.SaveChangesAsync(); + + Assert.True(await db.Set().AnyAsync(x => x.RetryFor == orphan.Id)); + Assert.True(await db.Set().AnyAsync(x => x.RetryFor == healthy.Id)); } + // ─── Full execution path ────────────────────────────────────────────────── + [Fact] - public async Task RetryJob_carries_group_attempt_counts_onto_retry_xchange() + public async Task RetryJob_processes_due_delayed_retry_and_creates_retry_xchange() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var xs = scope.ServiceProvider.GetRequiredService(); - var doc = new Document(8002, "RetryJob GroupCounts Doc"); + var doc = new Document(8001, "RetryJob Due Doc"); db.Set().Add(doc); await db.SaveChangesAsync(); - var sub = new Subscription("RetryJob GroupCounts Sub", doc.Id); + var sub = new Subscription("RetryJob Sub", doc.Id); sub.Inactive = false; db.Set().Add(sub); await db.SaveChangesAsync(); + // Use CreateXchange so the input file is uploaded to real cloud storage var originalXchange = await xs.CreateXchange(sub, new XchangeFile("{}")); - var groupId = Guid.NewGuid().ToString(); var delayedRetry = new DelayedRetry { Id = originalXchange.Id, - On = DateTime.UtcNow.AddMinutes(-1), - GroupAttemptCounts = new System.Collections.Generic.Dictionary - { - [groupId] = 2 - } + On = DateTime.UtcNow.AddMinutes(-1) }; db.Set().Add(delayedRetry); await db.SaveChangesAsync(); await BuildJob(db, xs).Execute(); + // DelayedRetry must be gone + var retryGone = !await db.Set().AnyAsync(r => r.Id == originalXchange.Id); + Assert.True(retryGone, "The processed DelayedRetry record must be deleted."); + + // A new Xchange with RetryFor pointing to the original must exist var retryXchange = await db.Set() .FirstOrDefaultAsync(x => x.RetryFor == originalXchange.Id); Assert.NotNull(retryXchange); - Assert.NotNull(retryXchange.GroupAttemptCounts); - Assert.True(retryXchange.GroupAttemptCounts.TryGetValue(groupId, out var count)); - Assert.Equal(2, count); + Assert.Equal(originalXchange.Id, retryXchange.RetryFor); + Assert.Equal(sub.Id, retryXchange.SubscriptionId); } [Fact] diff --git a/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs index 45f975f2..1c81406b 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using Microsoft.Extensions.DependencyInjection; using SW.Bitween.Domain; using SW.Bitween.IntegrationTests.Fixtures; @@ -24,10 +26,16 @@ public RetryPolicyTests(BitweenFixture fixture) // ─── Helpers ────────────────────────────────────────────────────────────── + private static AdapterSecretProperties Secrets(AsyncServiceScope scope) => + scope.ServiceProvider.GetRequiredService(); + + private static RetryUsageReport Report(AsyncServiceScope scope) => + scope.ServiceProvider.GetRequiredService(); + private static (Create create, Get get, Update update, Delete delete) - Handlers(BitweenDbContext db, RequestContext ctx) => ( + Handlers(BitweenDbContext db, RequestContext ctx, AdapterSecretProperties secrets) => ( new Create(db, ctx), - new Get(db), + new Get(db, ctx, secrets), new Update(db, ctx), new Delete(db, ctx)); @@ -59,8 +67,8 @@ public async Task Can_create_and_get_retry_policy() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var (create, get, _, _) = Handlers(db, ctx); + var ctx = scope.Superuser(); + var (create, get, _, _) = Handlers(db, ctx, Secrets(scope)); var id = (int)await create.Handle(SimplePolicy("Round-trip Policy")); @@ -77,8 +85,8 @@ public async Task Create_policy_with_complex_groups_round_trips_json_correctly() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var (create, _, _, _) = Handlers(db, ctx); + var ctx = scope.Superuser(); + var (create, _, _, _) = Handlers(db, ctx, Secrets(scope)); var policy = new RetryPolicyCreate { @@ -129,8 +137,8 @@ public async Task Can_update_retry_policy_name_and_groups() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var (create, _, update, _) = Handlers(db, ctx); + var ctx = scope.Superuser(); + var (create, _, update, _) = Handlers(db, ctx, Secrets(scope)); var id = (int)await create.Handle(SimplePolicy("Before Update")); @@ -168,8 +176,8 @@ public async Task Can_delete_retry_policy_not_assigned_to_any_subscription() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var (create, _, _, delete) = Handlers(db, ctx); + var ctx = scope.Superuser(); + var (create, _, _, delete) = Handlers(db, ctx, Secrets(scope)); var id = (int)await create.Handle(SimplePolicy("Deletable Policy")); @@ -186,8 +194,8 @@ public async Task Cannot_delete_retry_policy_that_is_assigned_to_a_subscription( { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var (create, _, _, delete) = Handlers(db, ctx); + var ctx = scope.Superuser(); + var (create, _, _, delete) = Handlers(db, ctx, Secrets(scope)); var doc = new Document(7001, "Delete Guard Doc"); db.Set().Add(doc); @@ -238,8 +246,8 @@ public async Task Subscription_retry_policy_id_is_persisted_and_fk_resolves() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var (create, _, _, _) = Handlers(db, ctx); + var ctx = scope.Superuser(); + var (create, _, _, _) = Handlers(db, ctx, Secrets(scope)); var doc = new Document(7002, "Sub FK Doc"); db.Set().Add(doc); @@ -333,14 +341,420 @@ public async Task Removing_retry_policy_nullifies_subscription_fk_via_set_null_c Assert.Null(reloaded.RetryPolicyId); } + // ─── Shared group total (MaxAttemptsTotal) ────────────────────────────────── + + [Fact] + public async Task Group_total_is_shared_across_separate_messages_of_the_same_integration() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7005, "Shared Total Doc"); + db.Set().Add(doc); + var sub = new Subscription("Shared Total Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 3, + MaxAttemptsTotal = 10, + DelayStrategy = new FixedDelayStrategy { DelayMs = 5_000 } + } + }; + var policy = new CustomRetryPolicy { Groups = [group] }; + + // Reproduces the reported bug: four failing messages, each retried up to its own + // per-message cap of 3, under a shared total of 10 — 12 retries before the fix. + var allowed = 0; + for (var message = 0; message < 4; message++) + for (var attempt = 0; attempt < 3; attempt++) + { + // A fresh evaluator and store per failure, exactly as XchangeService builds them. + var evaluator = new RetryPolicyEvaluator(policy, new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)); + var decision = await evaluator.Evaluate(XchangeResultType.Error, "timeout", attempt); + if (decision.ShouldRetry) allowed++; + await db.SaveChangesAsync(); + } + + Assert.Equal(10, allowed); + + var usage = await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == group.Id); + Assert.Equal(10, usage.AttemptsUsed); + } + + [Fact] + public async Task Group_total_is_tracked_per_integration_not_per_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7006, "Per Integration Doc"); + db.Set().Add(doc); + var subA = new Subscription("Per Integration Sub A", doc.Id); + var subB = new Subscription("Per Integration Sub B", doc.Id); + db.Set().AddRange(subA, subB); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 10, + MaxAttemptsTotal = 1, + DelayStrategy = new FixedDelayStrategy { DelayMs = 5_000 } + } + }; + var policy = new CustomRetryPolicy { Groups = [group] }; + + // Each integration gets its own single attempt, so one integration exhausting a + // shared policy template cannot starve the others. + Assert.True(await Allow(subA.Id)); + Assert.True(await Allow(subB.Id)); + Assert.False(await Allow(subA.Id)); + Assert.False(await Allow(subB.Id)); + return; + + async Task Allow(int subscriptionId) + { + var evaluator = new RetryPolicyEvaluator(policy, new RetryGroupBudget(db, scope.ServiceProvider, subscriptionId)); + var decision = await evaluator.Evaluate(XchangeResultType.Error, "timeout", 0); + await db.SaveChangesAsync(); + return decision.ShouldRetry; + } + } + + [Fact] + public async Task Concurrent_claims_never_exceed_the_group_total() + { + await using var setup = _fixture.CreateScope(); + var setupDb = setup.ServiceProvider.GetRequiredService(); + + var doc = new Document(7010, "Concurrent Budget Doc"); + setupDb.Set().Add(doc); + var sub = new Subscription("Concurrent Budget Sub", doc.Id); + setupDb.Set().Add(sub); + await setupDb.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + const int cap = 5; + const int racers = 16; + + // Bitween runs several instances, so simultaneous failures of the same integration and + // group are normal. Each racer gets its own scope and context, mimicking separate + // instances: a read-then-write would let several observe the same free slot at once. + var tasks = Enumerable.Range(0, racers).Select(async _ => + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, cap); + }); + + var claims = await Task.WhenAll(tasks); + var granted = claims.Count(claim => claim.Granted); + + Assert.Equal(cap, granted); + + var usage = await setupDb.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == groupId); + Assert.Equal(cap, usage.AttemptsUsed); + } + + // ─── Usage reporting and reset ────────────────────────────────────────────── + + [Fact] + public async Task Usage_reports_spent_budget_and_reset_clears_it() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7007, "Usage Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var policyId = (int)await new Create(db, ctx).Handle(SimplePolicy("Usage Policy")); + var saved = await db.Set().AsNoTracking().SingleAsync(p => p.Id == policyId); + var groupId = saved.Groups[0].Id; + + var sub = new Subscription("Usage Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + // Spend the whole budget (SimplePolicy allows 10 in total). + var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); + for (var i = 0; i < 10; i++) await budget.TryConsume(groupId, 10); + await db.SaveChangesAsync(); + + var rows = (List)await new Usage(db, ctx, Report(scope)).Handle(policyId, new RetryPolicyUsageRequest()); + var row = Assert.Single(rows); + Assert.Equal(sub.Id, row.SubscriptionId); + Assert.Equal("Usage Sub", row.SubscriptionName); + Assert.Equal("Timeout", row.GroupName); + Assert.Equal(10, row.AttemptsUsed); + Assert.True(row.Exhausted); + + await new ResetUsage(db, ctx).Handle(policyId, new RetryPolicyResetUsage + { + SubscriptionId = sub.Id, + GroupId = groupId + }); + + // The pair keeps its row — every subscription-and-group pair gets one so an alert override + // stays configurable before the first failure — but with nothing spent against the ceiling. + var afterReset = Assert.Single( + (List)await new Usage(db, ctx, Report(scope)).Handle(policyId, new RetryPolicyUsageRequest())); + Assert.Equal(0, afterReset.AttemptsUsed); + Assert.False(afterReset.Exhausted); + Assert.Null(afterReset.LastAttemptOn); + + // And the group can retry again. + Assert.True((await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, 10)).Granted); + } + + [Fact] + public async Task Usage_lists_never_failed_pairs_and_skips_groups_that_cannot_exhaust() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7011, "Never Failed Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var model = SimplePolicy("Never Failed Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + + // A Block group carries no budget, and the evaluator refuses before it ever claims one, so + // it can never exhaust and never alert. Reporting it would invite configuring an alert that + // cannot fire. + model.Groups.Add(new RetryGroup + { + Name = "Never retry", + Priority = 20, + Action = RetryAction.Block, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "fatal" }] + }); + + var policyId = (int)await new Create(db, ctx).Handle(model); + + var sub = new Subscription("Never Failed Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + var rows = (List)await new Usage(db, ctx, Report(scope)) + .Handle(policyId, new RetryPolicyUsageRequest()); + + // One row, not two: the pair is reported even though nothing has ever failed — otherwise its + // alert override would be unreachable until after the first failure — while the Block group + // is left out entirely. + var row = Assert.Single(rows); + Assert.Equal("Timeout", row.GroupName); + Assert.Equal(0, row.AttemptsUsed); + Assert.Equal(10, row.MaxAttemptsTotal); + Assert.False(row.Exhausted); + Assert.Null(row.LastAttemptOn); + Assert.Equal("NativeSmtpHandler", row.ResolvedHandlerId); + Assert.Equal(RetryAlertLevel.Policy, row.ResolvedFrom); + } + + [Fact] + public async Task Reset_does_not_touch_counters_of_another_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7008, "Reset Scope Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var mineId = (int)await new Create(db, ctx).Handle(SimplePolicy("Reset Scope Mine")); + var otherId = (int)await new Create(db, ctx).Handle(SimplePolicy("Reset Scope Other")); + var otherGroupId = (await db.Set().AsNoTracking() + .SingleAsync(p => p.Id == otherId)).Groups[0].Id; + + var otherSub = new Subscription("Reset Scope Other Sub", doc.Id); + db.Set().Add(otherSub); + await db.SaveChangesAsync(); + otherSub.SetRetryPolicy(otherId, null); + await db.SaveChangesAsync(); + + await new RetryGroupBudget(db, scope.ServiceProvider, otherSub.Id).TryConsume(otherGroupId, 10); + await db.SaveChangesAsync(); + + // Resetting everything under one policy must leave the other policy's counters alone. + await new ResetUsage(db, ctx).Handle(mineId, new RetryPolicyResetUsage()); + + // A row now exists for every pair whether or not it has failed, so assert the spent counter + // itself survived — row count alone would pass even if the reset had wrongly cleared it. + var otherRow = Assert.Single( + (List)await new Usage(db, ctx, Report(scope)).Handle(otherId, new RetryPolicyUsageRequest())); + Assert.Equal(1, otherRow.AttemptsUsed); + } + + [Fact] + public async Task Removing_a_group_clears_its_spent_budget() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7009, "Removed Group Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var policyId = (int)await new Create(db, ctx).Handle(SimplePolicy("Removed Group Policy")); + var saved = await db.Set().AsNoTracking().SingleAsync(p => p.Id == policyId); + var groupId = saved.Groups[0].Id; + + var sub = new Subscription("Removed Group Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, 10); + await db.SaveChangesAsync(); + Assert.True(await db.Set().AnyAsync(u => u.GroupId == groupId)); + + // Dropping the group must take its counter with it, or the row is stranded where + // neither the usage report nor reset can reach it. + await new Update(db, ctx).Handle(policyId, new RetryPolicyUpdate + { + Name = "Removed Group Policy", + Groups = [] + }); + + Assert.False(await db.Set().AnyAsync(u => u.GroupId == groupId)); + } + + // ─── Attempts drill-down ──────────────────────────────────────────────────── + + [Fact] + public async Task Attempts_lists_only_this_pairs_stamped_failures_pending_first() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7012, "Attempts Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var policyId = (int)await new Create(db, ctx).Handle(SimplePolicy("Attempts Policy")); + var groupId = (await db.Set().AsNoTracking() + .SingleAsync(p => p.Id == policyId)).Groups[0].Id; + + var sub = new Subscription("Attempts Sub", doc.Id); + var otherSub = new Subscription("Attempts Other Sub", doc.Id); + db.Set().AddRange(sub, otherSub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + otherSub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + // Still being worked on: a scheduled retry is outstanding for it. + var pending = new Xchange(sub, new XchangeFile("{}")); + var pendingResult = new XchangeResult(pending.Id, null, null, exception: "first timeout"); + pendingResult.SetRetryEvaluation(groupId, 0); + + // Given up on, and the reason recorded. + var stopped = new Xchange(sub, new XchangeFile("{}")); + var stoppedResult = new XchangeResult(stopped.Id, null, null, exception: "second timeout"); + stoppedResult.SetRetryEvaluation(groupId, 1); + stoppedResult.SetRetryBlocked("Group 'Timeout' has used all 10 of its total attempts"); + + // Carries no group: this is what every failure recorded before the group was stamped onto + // results looks like, and it has no pair to be listed under. + var unstamped = new Xchange(sub, new XchangeFile("{}")); + var unstampedResult = new XchangeResult(unstamped.Id, null, null, exception: "older timeout"); + + // Same policy and same group, different subscription — a row of its own, not this one's. + var otherPair = new Xchange(otherSub, new XchangeFile("{}")); + var otherPairResult = new XchangeResult(otherPair.Id, null, null, exception: "someone else's timeout"); + otherPairResult.SetRetryEvaluation(groupId, 0); + + db.Set().AddRange(pending, stopped, unstamped, otherPair); + db.Set().AddRange(pendingResult, stoppedResult, unstampedResult, otherPairResult); + db.Set().Add(new DelayedRetry { Id = pending.Id, On = DateTime.UtcNow.AddMinutes(5) }); + await db.SaveChangesAsync(); + + var result = (RetryGroupAttempts)await new Attempts(db, ctx).Handle(policyId, + new RetryGroupAttemptsRequest { SubscriptionId = sub.Id, GroupId = groupId }); + + // Two, not four: the unstamped failure and the other subscription's are both out. + Assert.Equal(2, result.Total); + Assert.Equal(2, result.Attempts.Count); + + // Pending leads, so a long history of finished failures can never push the one still moving + // out of a capped list. + Assert.Equal(pending.Id, result.Attempts[0].XchangeId); + Assert.True(result.Attempts[0].RetryPending); + Assert.Equal(0, result.Attempts[0].AttemptNumber); + Assert.Equal("first timeout", result.Attempts[0].Exception); + Assert.Null(result.Attempts[0].RetryBlockedReason); + + Assert.Equal(stopped.Id, result.Attempts[1].XchangeId); + Assert.False(result.Attempts[1].RetryPending); + Assert.Equal(1, result.Attempts[1].AttemptNumber); + Assert.Contains("used all 10", result.Attempts[1].RetryBlockedReason); + } + + [Fact] + public async Task Attempts_rejects_a_subscription_that_does_not_use_the_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7013, "Attempts Scope Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var mineId = (int)await new Create(db, ctx).Handle(SimplePolicy("Attempts Scope Mine")); + var theirsId = (int)await new Create(db, ctx).Handle(SimplePolicy("Attempts Scope Theirs")); + var theirGroupId = (await db.Set().AsNoTracking() + .SingleAsync(p => p.Id == theirsId)).Groups[0].Id; + + var theirSub = new Subscription("Attempts Scope Their Sub", doc.Id); + db.Set().Add(theirSub); + await db.SaveChangesAsync(); + theirSub.SetRetryPolicy(theirsId, null); + await db.SaveChangesAsync(); + + // Asking one policy for another policy's subscription must fail rather than quietly answer: + // the route key is what the caller was authorised against. + await Assert.ThrowsAsync(() => new Attempts(db, ctx).Handle(mineId, + new RetryGroupAttemptsRequest { SubscriptionId = theirSub.Id, GroupId = theirGroupId })); + } + // ─── Test / dry-run endpoint ──────────────────────────────────────────────── [Fact] public async Task Test_simulates_consecutive_attempts_and_stops_once_blocked() { await using var scope = _fixture.CreateScope(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var handler = new Resources.RetryPolicies.Test(ctx); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + var handler = new Resources.RetryPolicies.Test(db, ctx); var request = new TestRetryPolicyRequest { @@ -369,8 +783,9 @@ public async Task Test_simulates_consecutive_attempts_and_stops_once_blocked() public async Task Test_rejects_success_result_type() { await using var scope = _fixture.CreateScope(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var handler = new Resources.RetryPolicies.Test(ctx); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + var handler = new Resources.RetryPolicies.Test(db, ctx); var request = new TestRetryPolicyRequest { @@ -386,8 +801,9 @@ public async Task Test_rejects_success_result_type() public async Task Test_reports_no_match_when_no_group_applies() { await using var scope = _fixture.CreateScope(); - var ctx = scope.ServiceProvider.GetRequiredService(); - var handler = new Resources.RetryPolicies.Test(ctx); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + var handler = new Resources.RetryPolicies.Test(db, ctx); var request = new TestRetryPolicyRequest { @@ -404,4 +820,687 @@ public async Task Test_reports_no_match_when_no_group_applies() Assert.Null(response.Attempts[0].MatchedGroupName); } + // ─── Exhaustion alert claim ───────────────────────────────────────────────── + + [Fact] + public async Task Exhausting_a_budget_claims_the_alert_exactly_once() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7101, "Alert Claim Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription("Alert Claim Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); + + // Spending the budget never alerts — nothing has been refused yet. + for (var i = 0; i < 3; i++) + { + var spending = await budget.TryConsume(groupId, 3); + Assert.True(spending.Granted); + Assert.False(spending.JustExhausted); + } + + // The first refusal owns the alert. + var first = await budget.TryConsume(groupId, 3); + Assert.False(first.Granted); + Assert.True(first.JustExhausted); + + // Every refusal after it stays quiet, however many failures arrive. + var second = await budget.TryConsume(groupId, 3); + Assert.False(second.Granted); + Assert.False(second.JustExhausted); + + var usage = await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == groupId); + Assert.NotNull(usage.ExhaustedNotifiedOn); + } + + [Fact] + public async Task Concurrent_refusals_claim_the_alert_only_once() + { + await using var setupScope = _fixture.CreateScope(); + var setupDb = setupScope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7102, "Alert Race Doc"); + setupDb.Set().Add(doc); + await setupDb.SaveChangesAsync(); + + var sub = new Subscription("Alert Race Sub", doc.Id); + setupDb.Set().Add(sub); + await setupDb.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + + // Spends the only attempt, so every racer below meets an empty budget. Asserted, or a + // failure here would surface as a confusing claim count further down. + var setupClaim = await new RetryGroupBudget(setupDb, setupScope.ServiceProvider, sub.Id) + .TryConsume(groupId, 1); + Assert.True(setupClaim.Granted); + + // Several instances can discover the empty budget in the same instant; a read-then-write + // would let each of them decide it was the first and send its own email. + var tasks = Enumerable.Range(0, 12).Select(async _ => + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, 1); + }); + + var claims = await Task.WhenAll(tasks); + + Assert.Equal(1, claims.Count(c => c.JustExhausted)); + Assert.DoesNotContain(claims, c => c.Granted); + } + + [Fact] + public async Task Resetting_usage_re_arms_the_exhaustion_alert() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7103, "Alert Rearm Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var policyId = (int)await new Create(db, ctx).Handle(SimplePolicy("Alert Rearm Policy")); + var saved = await db.Set().AsNoTracking().SingleAsync(p => p.Id == policyId); + var groupId = saved.Groups[0].Id; + + var sub = new Subscription("Alert Rearm Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); + for (var i = 0; i < 10; i++) await budget.TryConsume(groupId, 10); + Assert.True((await budget.TryConsume(groupId, 10)).JustExhausted); + + await new ResetUsage(db, ctx).Handle(policyId, new RetryPolicyResetUsage + { + SubscriptionId = sub.Id, + GroupId = groupId + }); + + // Reset deletes the row, so the budget and its alert come back together. + for (var i = 0; i < 10; i++) await budget.TryConsume(groupId, 10); + Assert.True((await budget.TryConsume(groupId, 10)).JustExhausted); + } + + // ─── Alert config validation ─────────────────────────────────────────────── + + [Fact] + public async Task Cannot_save_a_group_that_sends_its_own_alert_without_a_handler() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var model = SimplePolicy("Alert Validation Policy"); + model.Groups = + [ + new RetryGroup + { + Name = model.Groups[0].Name, + Priority = model.Groups[0].Priority, + AppliesTo = model.Groups[0].AppliesTo, + Matchers = model.Groups[0].Matchers, + Budget = model.Groups[0].Budget, + AlertMode = RetryAlertMode.Send + } + ]; + + await Assert.ThrowsAsync(() => new Create(db, ctx).Handle(model)); + } + + // ─── Reaching an inline policy's counters ──────────────────────────────────── + + [Fact] + public async Task An_inline_policy_budget_can_be_reported_and_reset_by_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7015, "Inline Policy Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription("Inline Policy Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + // Carried on the subscription itself, so there is no policy id anywhere to ask about it. + var inline = new CustomRetryPolicy { Groups = SimplePolicy("unused").Groups }; + sub.SetRetryPolicy(null, inline); + await db.SaveChangesAsync(); + + var groupId = inline.Groups[0].Id; + + // Spends the whole shared budget, which is what stops this subscription retrying at all. + for (var i = 0; i < 10; i++) + await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, 10); + await db.SaveChangesAsync(); + + var row = Assert.Single((List)await new Resources.Subscriptions.RetryUsage( + db, ctx, Report(scope)).Handle(sub.Id, new RetryPolicyUsageRequest())); + + Assert.Equal(10, row.AttemptsUsed); + Assert.True(row.Exhausted); + + // An inline policy has no row to hold a policy-level alert, so nothing resolves from there — + // and that has to read as "nothing configured" rather than as a level being consulted. + Assert.Null(row.ResolvedHandlerId); + Assert.Null(row.ResolvedFrom); + + // The point of the whole endpoint: before this, no reset could reach these counters, so the + // subscription stayed stopped for good. + await new Resources.Subscriptions.ResetRetryUsage(db, ctx) + .Handle(sub.Id, new SubscriptionRetryResetUsage()); + + var afterReset = Assert.Single((List)await new Resources.Subscriptions.RetryUsage( + db, ctx, Report(scope)).Handle(sub.Id, new RetryPolicyUsageRequest())); + Assert.Equal(0, afterReset.AttemptsUsed); + Assert.False(afterReset.Exhausted); + + // And it stays scoped to this subscription: a policy-scoped report must still not see it, + // which is precisely why the subscription-scoped one had to exist. + Assert.False(await db.Set().AnyAsync(u => u.SubscriptionId == sub.Id)); + } + + // ─── Allow with no budget ─────────────────────────────────────────────────── + + [Fact] + public async Task Allow_without_a_budget_is_rejected_on_save() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + RetryPolicyCreate PolicyWithBudgetlessGroup(string name, RetryAction action) => new() + { + Name = name, + Groups = + [ + new RetryGroup + { + Name = "No budget", + Priority = 10, + Action = action, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }] + } + ] + }; + + // Nothing to work from — no caps, no delay — so the evaluator could only ever refuse it, and + // refusing quietly reads as retries being broken. Rejected where it is configured instead. + await Assert.ThrowsAsync( + () => new Create(db, ctx).Handle(PolicyWithBudgetlessGroup("Budgetless Allow", RetryAction.Allow))); + + // Block is the shape that legitimately has no budget, and it must still save. + await new Create(db, ctx).Handle(PolicyWithBudgetlessGroup("Budgetless Block", RetryAction.Block)); + } + + // ─── Alert secrets ────────────────────────────────────────────────────────── + + // What the browser is shown in place of a secret. Spelled out rather than taken from the + // constant: the UI has its own copy of this string, and the two have to stay the same. + private const string Sentinel = "__private__"; + + private static Dictionary SmtpProperties(string password, bool useTls) => new() + { + ["Host"] = "localhost", + ["Port"] = "1025", + ["UseTls"] = useTls ? "true" : "false", + ["Password"] = password, + ["From"] = "bitween-alerts@example.com", + ["To"] = "ops@example.com", + ["Subject"] = "Retries stopped", + ["Body"] = "Budget spent." + }; + + [Fact] + public async Task An_alert_password_is_masked_on_read_and_survives_being_saved_back() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var model = SimplePolicy("Masked Alert Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + model.AlertHandlerProperties = SmtpProperties("hunter2", useTls: true); + + var policyId = (int)await new Create(db, ctx).Handle(model); + + var loaded = (RetryPolicyUpdate)await new Get(db, ctx, Secrets(scope)).Handle(policyId); + + // The password never leaves the server; everything that is not a secret still does, or the + // form would have nothing to show. + Assert.Equal(Sentinel, loaded.AlertHandlerProperties["Password"]); + Assert.Equal("localhost", loaded.AlertHandlerProperties["Host"]); + Assert.Equal("Retries stopped", loaded.AlertHandlerProperties["Subject"]); + + // Exactly what the page does when someone edits the subject and saves: the password comes + // back as the mask, and must not be stored as one. + loaded.AlertHandlerProperties["Subject"] = "Retries stopped for real"; + await new Update(db, ctx).Handle(policyId, new RetryPolicyUpdate + { + Name = loaded.Name, + Groups = loaded.Groups, + AlertHandlerId = loaded.AlertHandlerId, + AlertHandlerProperties = loaded.AlertHandlerProperties + }); + + var stored = await db.Set().AsNoTracking().SingleAsync(p => p.Id == policyId); + Assert.Equal("hunter2", stored.AlertHandlerProperties["Password"]); + Assert.Equal("Retries stopped for real", stored.AlertHandlerProperties["Subject"]); + } + + [Fact] + public async Task Overriding_an_inherited_alert_keeps_the_password_it_was_only_shown_masked() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7014, "Copied Secret Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var model = SimplePolicy("Copied Secret Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + model.AlertHandlerProperties = SmtpProperties("hunter2", useTls: true); + var policyId = (int)await new Create(db, ctx).Handle(model); + + var groupId = (await db.Set().AsNoTracking() + .SingleAsync(p => p.Id == policyId)).Groups[0].Id; + + var sub = new Subscription("Copied Secret Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + var row = Assert.Single((List)await new Usage(db, ctx, Report(scope)) + .Handle(policyId, new RetryPolicyUsageRequest())); + Assert.Equal(Sentinel, row.ResolvedHandlerProperties["Password"]); + + // The page offers "start from what this currently sends", so the masked value is what comes + // back — and there is no override row yet to restore it from. It has to be recovered from the + // level the caller was shown it at, or the new override would send with no password at all. + await new SaveAlertOverride(db, ctx, Secrets(scope)).Handle(policyId, new RetryAlertOverrideSave + { + SubscriptionId = sub.Id, + GroupId = groupId, + AlertMode = RetryAlertMode.Send, + AlertHandlerId = "NativeSmtpHandler", + AlertHandlerProperties = row.ResolvedHandlerProperties + }); + + var stored = await db.Set().AsNoTracking() + .SingleAsync(o => o.SubscriptionId == sub.Id && o.GroupId == groupId); + Assert.Equal("hunter2", stored.AlertHandlerProperties["Password"]); + Assert.Equal("ops@example.com", stored.AlertHandlerProperties["To"]); + } + + [Fact] + public async Task A_mail_alert_with_a_password_and_no_encryption_is_rejected_on_save() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var model = SimplePolicy("Cleartext Alert Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + model.AlertHandlerProperties = SmtpProperties("hunter2", useTls: false); + + // Caught on save, where the person configuring it is looking — the handler refuses this at + // send time too, but by then the only trace is a missing alert. + await Assert.ThrowsAsync(() => new Create(db, ctx).Handle(model)); + + // Encryption off is fine on its own; it is only the password that must not travel in clear. + model.AlertHandlerProperties = SmtpProperties("", useTls: false); + await new Create(db, ctx).Handle(model); + } + + [Fact] + public async Task Policy_alert_handler_round_trips() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var model = SimplePolicy("Alert Handler Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + model.AlertHandlerProperties = new Dictionary { ["to"] = "ops@example.com" }; + + var policyId = (int)await new Create(db, ctx).Handle(model); + var loaded = (RetryPolicyUpdate)await new Get(db, ctx, Secrets(scope)).Handle(policyId); + + Assert.Equal("NativeSmtpHandler", loaded.AlertHandlerId); + Assert.Equal("ops@example.com", loaded.AlertHandlerProperties["to"]); + } + + // ─── Manual retries and the shared budget ───────────────────────────────── + + /// + /// A person pressing Retry must not spend the budget set aside for unattended retries. + /// + /// + /// Both attempts in this test are children of the same failed exchange, fail the same way against + /// the same group, and differ only in who asked for them. Without that pairing the test could pass + /// simply because nothing was ever evaluated. + /// + [Fact] + public async Task A_retry_started_by_hand_is_left_alone_by_the_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + const string failure = "manual retry budget probe failed"; + + var doc = new Document(7031, "Manual Retry Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + var policy = new RetryPolicy + { + Name = "Manual Retry Policy " + Guid.NewGuid().ToString("N")[..6], + Groups = + [ + new RetryGroup + { + Id = groupId, + Name = "Probe", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = failure }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 3, + MaxAttemptsTotal = 5, + DelayStrategy = new FixedDelayStrategy { DelayMs = 60_000 } + } + } + ] + }; + db.Set().Add(policy); + await db.SaveChangesAsync(); + + // A handler that fails on demand, so the failure text is chosen here rather than inherited + // from whatever the environment happens to throw, and the matcher above can be exact. + var sub = new Subscription("Manual Retry Sub", doc.Id); + sub.HandlerId = "sw.bitween.sampleconfigurableadapter"; + sub.SetDictionaries( + new Dictionary { ["SimulateError"] = "true", ["ErrorMessage"] = failure }, + null, null, null, null); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policy.Id, null); + await db.SaveChangesAsync(); + + + // The document cache is a warm singleton shared by the whole collection, and production + // clears it over the bus whenever a document changes. Cleared here for the same reason: a + // document created after the cache warmed is invisible to the filter step, which then fails + // on its own before any handler runs. + scope.ServiceProvider.GetRequiredService().Revoke(); + + var original = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + // One of each, exactly as their callers build them: the endpoint behind the Retry button, and + // RetryJob working through a due DelayedRetry. + await xs.CreateXchange(sub, original, new XchangeFile("{}"), manualRetry: true); + await xs.CreateXchange(sub, original, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + var children = await db.Set().AsNoTracking() + .Where(x => x.RetryFor == original.Id).ToListAsync(); + var byHand = Assert.Single(children, x => x.ManualRetry); + var byPolicy = Assert.Single(children, x => !x.ManualRetry); + + await Run(byHand.Id); + await Run(byPolicy.Id); + + var handResult = await db.Set().AsNoTracking().SingleAsync(r => r.Id == byHand.Id); + var policyResult = await db.Set().AsNoTracking().SingleAsync(r => r.Id == byPolicy.Id); + + // Both genuinely failed, and failed the way the group is written for, so the policy had + // something to match in either case. + Assert.False(handResult.Success); + Assert.False(policyResult.Success); + Assert.Contains(failure, handResult.Exception); + Assert.Contains(failure, policyResult.Exception); + + Assert.Null(handResult.RetryGroupId); + Assert.Contains("by hand", handResult.RetryBlockedReason); + Assert.False(await db.Set().AsNoTracking().AnyAsync(r => r.Id == byHand.Id)); + + // The control: the same failure, evaluated, charged for and scheduled. + Assert.Equal(groupId, policyResult.RetryGroupId); + Assert.True(await db.Set().AsNoTracking().AnyAsync(r => r.Id == byPolicy.Id)); + + var usage = await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == groupId); + Assert.Equal(1, usage.AttemptsUsed); + + // Runs the exchange through the same entry point the bus calls, so the guard is exercised + // where it actually sits rather than through a seam opened up for the test. + async Task Run(string xchangeId) + { + await using var runScope = _fixture.CreateScope(); + await runScope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = xchangeId })); + } + } + + // ─── Recovery ───────────────────────────────────────────────────────────── + + /// + /// A success is what tells Bitween the downstream is back, so it is what gives the budget back. + /// + /// + /// Nothing else can: an exhausted group schedules no more retries, so no retry will ever succeed + /// to report the recovery. Only ordinary traffic getting through can, which is what this drives. + /// + [Fact] + public async Task A_success_gives_the_group_its_spent_budget_back() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7032, "Recovery Doc"); + db.Set().Add(doc); + var sub = new Subscription("Recovery Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 5, + MaxAttemptsTotal = 2, + DelayStrategy = new FixedDelayStrategy { DelayMs = 60_000 } + } + }; + // Attached to the subscription, not just handed to the evaluator: releasing a budget reads the + // group's cap back from the policy the subscription actually holds, because a total that is + // only partly spent must be left alone. + sub.SetRetryPolicy(null, new CustomRetryPolicy { Groups = [group] }); + await db.SaveChangesAsync(); + + async Task Fail() => + await new RetryPolicyEvaluator(sub.CustomRetryPolicy, + new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)) + .Evaluate(XchangeResultType.Error, "System.TimeoutException: timeout", 0); + + Assert.True((await Fail()).ShouldRetry); + Assert.True((await Fail()).ShouldRetry); + + var exhausted = await Fail(); + Assert.False(exhausted.ShouldRetry); + Assert.True(exhausted.BudgetJustExhausted); + + + // The document cache is a warm singleton shared by the whole collection, and production + // clears it over the bus whenever a document changes. Cleared here for the same reason: a + // document created after the cache warmed is invisible to the filter step, which then fails + // on its own before any handler runs. + scope.ServiceProvider.GetRequiredService().Revoke(); + + // The subscription has no handler, so this exchange simply succeeds — an ordinary message + // getting through after the outage, which is the only evidence of recovery there is. + var recovered = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + await using (var runScope = _fixture.CreateScope()) + await runScope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = recovered.Id })); + + var result = await db.Set().AsNoTracking().SingleAsync(r => r.Id == recovered.Id); + Assert.True(result.Success); + + Assert.Empty(await db.Set().AsNoTracking() + .Where(u => u.SubscriptionId == sub.Id).ToListAsync()); + + // Retrying works again, and because the row is gone the next exhaustion alerts afresh. + var afterRecovery = await Fail(); + Assert.True(afterRecovery.ShouldRetry); + } + + /// + /// A total that is only partly spent is not credited back by an ordinary success. + /// + /// + /// The cap is there for a downstream that fails some messages and succeeds others. Handing the + /// total back on every success would mean exactly that downstream never reaches its cap, so the + /// release is deliberately limited to a budget that has actually run out. + /// + [Fact] + public async Task A_partly_spent_budget_is_left_alone_by_a_success() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7033, "Partly Spent Doc"); + db.Set().Add(doc); + var sub = new Subscription("Partly Spent Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 5, + MaxAttemptsTotal = 4, + DelayStrategy = new FixedDelayStrategy { DelayMs = 60_000 } + } + }; + sub.SetRetryPolicy(null, new CustomRetryPolicy { Groups = [group] }); + await db.SaveChangesAsync(); + + // One of four spent, so the group is still allowed to retry and has nothing to recover from. + var spend = await new RetryPolicyEvaluator(sub.CustomRetryPolicy, + new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)) + .Evaluate(XchangeResultType.Error, "System.TimeoutException: timeout", 0); + Assert.True(spend.ShouldRetry); + + scope.ServiceProvider.GetRequiredService().Revoke(); + + var succeeded = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + await using (var runScope = _fixture.CreateScope()) + await runScope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = succeeded.Id })); + + Assert.True((await db.Set().AsNoTracking().SingleAsync(r => r.Id == succeeded.Id)).Success); + + var usage = await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == group.Id); + Assert.Equal(1, usage.AttemptsUsed); + } + + /// + /// A slot charged after the success began is not handed back by it. + /// + /// + /// Bitween runs several instances, so a failure can claim a slot while a success is still being + /// processed. Releasing that row would give back a slot already spent and let the group retry past + /// its total. The row's last attempt is compared against the successful exchange's start, which is + /// what this drives directly — the timing is otherwise a race no test could pin down. + /// + [Fact] + public async Task A_slot_charged_after_the_success_began_is_not_handed_back() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7034, "Watermark Doc"); + db.Set().Add(doc); + var sub = new Subscription("Watermark Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 5, + MaxAttemptsTotal = 1, + DelayStrategy = new FixedDelayStrategy { DelayMs = 60_000 } + } + }; + sub.SetRetryPolicy(null, new CustomRetryPolicy { Groups = [group] }); + await db.SaveChangesAsync(); + + // Exhausted, and charged after the moment the success below claims to have started. + db.Set().Add(new RetryGroupUsage + { + SubscriptionId = sub.Id, + GroupId = group.Id, + AttemptsUsed = 1, + LastAttemptOn = DateTime.UtcNow.AddMinutes(5) + }); + await db.SaveChangesAsync(); + + var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); + + Assert.Equal(0, await budget.ReleaseExhaustedBudgets(DateTime.UtcNow)); + Assert.Equal(1, (await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id)).AttemptsUsed); + + // The same budget, released once the success is known to postdate the charge. + Assert.Equal(1, await budget.ReleaseExhaustedBudgets(DateTime.UtcNow.AddMinutes(10))); + Assert.Empty(await db.Set().AsNoTracking() + .Where(u => u.SubscriptionId == sub.Id).ToListAsync()); + } } diff --git a/SW.Bitween.MsSql/BitweenDbContext.cs b/SW.Bitween.MsSql/BitweenDbContext.cs index 6088c1ef..ddef6d38 100644 --- a/SW.Bitween.MsSql/BitweenDbContext.cs +++ b/SW.Bitween.MsSql/BitweenDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; using SW.PrimitiveTypes; using SW.Scheduler.SqlServer; @@ -6,6 +7,9 @@ namespace SW.Bitween.MsSql { public class BitweenDbContext : Bitween.BitweenDbContext { + /// Backs ids — see the note in OnModelCreating. + public const string DocumentIdSequence = "DocumentIds"; + public BitweenDbContext(DbContextOptions options, RequestContext requestContext, IPublish publish) : base(options, requestContext, publish) { } @@ -13,6 +17,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.UseSchedulerSqlServer(); + + // Documents.Id became database-generated after the table already existed. SQL Server + // cannot add IDENTITY to an existing column — EF refuses outright ("to change the + // IDENTITY property of a column, the column needs to be dropped and recreated") — and + // rebuilding Documents would mean dropping the foreign keys Subscriptions and Xchanges + // hold against it. A sequence-backed default generates ids the same way with no table + // rebuild. Postgres uses identity and MySQL AUTO_INCREMENT, which both alter in place. + modelBuilder.Entity().Property(p => p.Id).UseSequence(DocumentIdSequence); } } } diff --git a/SW.Bitween.MsSql/Migrations/20260726123726_AddDocumentCodeAndAutoId.Designer.cs b/SW.Bitween.MsSql/Migrations/20260726123726_AddDocumentCodeAndAutoId.Designer.cs new file mode 100644 index 00000000..2d40e09e --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260726123726_AddDocumentCodeAndAutoId.Designer.cs @@ -0,0 +1,1904 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260726123726_AddDocumentCodeAndAutoId")] + partial class AddDocumentCodeAndAutoId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260726123726_AddDocumentCodeAndAutoId.cs b/SW.Bitween.MsSql/Migrations/20260726123726_AddDocumentCodeAndAutoId.cs new file mode 100644 index 00000000..1d8071d7 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260726123726_AddDocumentCodeAndAutoId.cs @@ -0,0 +1,82 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddDocumentCodeAndAutoId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "DocumentIds"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Documents", + type: "int", + nullable: false, + defaultValueSql: "NEXT VALUE FOR [DocumentIds]", + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AddColumn( + name: "Code", + table: "Documents", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true); + + migrationBuilder.UpdateData( + table: "Documents", + keyColumn: "Id", + keyValue: 10001, + column: "Code", + value: null); + + migrationBuilder.CreateIndex( + name: "IX_Documents_Code", + table: "Documents", + column: "Code", + unique: true, + filter: "[Code] IS NOT NULL"); + + // The sequence starts at 1; realign it past the existing (user-assigned, pre-sequence) + // ids so the next INSERT can't collide with them. RESTART WITH needs a literal, hence + // the dynamic SQL. + migrationBuilder.Sql(""" + DECLARE @next bigint = (SELECT ISNULL(MAX([Id]), 0) + 1 FROM [Documents]); + DECLARE @sql nvarchar(200) = + N'ALTER SEQUENCE [DocumentIds] RESTART WITH ' + CAST(@next AS nvarchar(20)); + EXEC sp_executesql @sql; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Documents_Code", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "Code", + table: "Documents"); + + migrationBuilder.DropSequence( + name: "DocumentIds"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Documents", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int", + oldDefaultValueSql: "NEXT VALUE FOR [DocumentIds]"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.Designer.cs b/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.Designer.cs new file mode 100644 index 00000000..0fb443f7 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.Designer.cs @@ -0,0 +1,2006 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260726123818_AddRolesAndPermissions")] + partial class AddRolesAndPermissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260726123818_AddRolesAndPermissions.cs b/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.cs new file mode 100644 index 00000000..33fa07c5 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddRolesAndPermissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Permissions = table.Column(type: "nvarchar(max)", nullable: true), + IsSystem = table.Column(type: "bit", nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + ModifiedOn = table.Column(type: "datetime2", nullable: true), + ModifiedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AccountRoles", + columns: table => new + { + AccountId = table.Column(type: "int", nullable: false), + RoleId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountRoles", x => new { x.AccountId, x.RoleId }); + table.ForeignKey( + name: "FK_AccountRoles_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AccountRoles_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Roles", + columns: new[] { "Id", "CreatedBy", "CreatedOn", "Description", "IsSystem", "ModifiedBy", "ModifiedOn", "Name", "Permissions" }, + values: new object[,] + { + { 1, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Full access to everything, including members, roles and settings.", true, null, null, "Administrator", "[]" }, + { 2, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Runs and configures integrations. Can't manage members, roles or settings.", true, null, null, "Member", "[]" }, + { 3, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Read-only access to integrations, exchanges and configuration.", true, null, null, "Viewer", "[]" } + }); + + migrationBuilder.CreateIndex( + name: "IX_AccountRoles_RoleId", + table: "AccountRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_Roles_Name", + table: "Roles", + column: "Name", + unique: true); + + // Every existing account keeps exactly the access it had: its coarse AccountRole + // (Admin=0, Viewer=10, Member=20) becomes the matching built-in role. Anything + // unrecognised lands on Viewer — least privilege. Idempotent, so re-running or + // ordering against the seeded admin account can't produce duplicates. + migrationBuilder.Sql(""" + INSERT INTO [AccountRoles] ([AccountId], [RoleId]) + SELECT a.[Id], CASE a.[Role] WHEN 0 THEN 1 WHEN 20 THEN 2 ELSE 3 END + FROM [Accounts] a + WHERE NOT EXISTS ( + SELECT 1 FROM [AccountRoles] l WHERE l.[AccountId] = a.[Id]); + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccountRoles"); + + migrationBuilder.DropTable( + name: "Roles"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260726151528_DropAccountPhone.Designer.cs b/SW.Bitween.MsSql/Migrations/20260726151528_DropAccountPhone.Designer.cs new file mode 100644 index 00000000..b4c7b400 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260726151528_DropAccountPhone.Designer.cs @@ -0,0 +1,2001 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260726151528_DropAccountPhone")] + partial class DropAccountPhone + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260726151528_DropAccountPhone.cs b/SW.Bitween.MsSql/Migrations/20260726151528_DropAccountPhone.cs new file mode 100644 index 00000000..00cb7c14 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260726151528_DropAccountPhone.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class DropAccountPhone : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Phone", + table: "Accounts"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Phone", + table: "Accounts", + type: "varchar(20)", + unicode: false, + maxLength: 20, + nullable: true); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + column: "Phone", + value: null); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260727133106_AddSettings.Designer.cs b/SW.Bitween.MsSql/Migrations/20260727133106_AddSettings.Designer.cs new file mode 100644 index 00000000..690c1db1 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260727133106_AddSettings.Designer.cs @@ -0,0 +1,2028 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260727133106_AddSettings")] + partial class AddSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260727133106_AddSettings.cs b/SW.Bitween.MsSql/Migrations/20260727133106_AddSettings.cs new file mode 100644 index 00000000..82c5365a --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260727133106_AddSettings.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Settings", + columns: table => new + { + Id = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true), + CreatedOn = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + ModifiedOn = table.Column(type: "datetime2", nullable: true), + ModifiedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Settings", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Settings"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.Designer.cs b/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.Designer.cs new file mode 100644 index 00000000..ca317971 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.Designer.cs @@ -0,0 +1,1902 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811081235_SharedRetryGroupTotals")] + partial class SharedRetryGroupTotals + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs b/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs new file mode 100644 index 00000000..f52cc72f --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class SharedRetryGroupTotals : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "Xchanges"); + + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "DelayedRetries"); + + migrationBuilder.CreateTable( + name: "RetryGroupUsages", + columns: table => new + { + SubscriptionId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "uniqueidentifier", nullable: false), + AttemptsUsed = table.Column(type: "int", nullable: false), + LastAttemptOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RetryGroupUsages", x => new { x.SubscriptionId, x.GroupId }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RetryGroupUsages"); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "Xchanges", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "DelayedRetries", + type: "nvarchar(max)", + nullable: true); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.Designer.cs b/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.Designer.cs new file mode 100644 index 00000000..8608dffa --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.Designer.cs @@ -0,0 +1,1906 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811092327_RetryBlockedReason")] + partial class RetryBlockedReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260811092327_RetryBlockedReason.cs b/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.cs new file mode 100644 index 00000000..4dc41bbf --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class RetryBlockedReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RetryBlockedReason", + table: "XchangeResults", + type: "nvarchar(500)", + maxLength: 500, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RetryBlockedReason", + table: "XchangeResults"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.Designer.cs b/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.Designer.cs new file mode 100644 index 00000000..a61e46f5 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.Designer.cs @@ -0,0 +1,1896 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260812092708_AddAccountLockout")] + partial class AddAccountLockout + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260812092708_AddAccountLockout.cs b/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.cs new file mode 100644 index 00000000..662b1a62 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddAccountLockout : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FailedLoginCount", + table: "Accounts", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "LockoutEnd", + table: "Accounts", + type: "datetime2", + nullable: true); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + columns: new[] { "FailedLoginCount", "LockoutEnd" }, + values: new object[] { 0, null }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FailedLoginCount", + table: "Accounts"); + + migrationBuilder.DropColumn( + name: "LockoutEnd", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.Designer.cs b/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.Designer.cs new file mode 100644 index 00000000..a504f677 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.Designer.cs @@ -0,0 +1,1956 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260817103506_RetryBudgetAlerts")] + partial class RetryBudgetAlerts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260817103506_RetryBudgetAlerts.cs b/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.cs new file mode 100644 index 00000000..e6d3d633 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.cs @@ -0,0 +1,122 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class RetryBudgetAlerts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AttemptNumber", + table: "XchangeResults", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "RetryGroupId", + table: "XchangeResults", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.AlterColumn( + name: "NotifierId", + table: "XchangeNotifications", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AddColumn( + name: "AlertHandlerId", + table: "RetryPolicies", + type: "varchar(200)", + unicode: false, + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "AlertHandlerProperties", + table: "RetryPolicies", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "ExhaustedNotifiedOn", + table: "RetryGroupUsages", + type: "datetime2", + nullable: true); + + migrationBuilder.CreateTable( + name: "RetryAlertOverrides", + columns: table => new + { + SubscriptionId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "uniqueidentifier", nullable: false), + AlertMode = table.Column(type: "tinyint", nullable: false), + AlertHandlerId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: true), + AlertHandlerProperties = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RetryAlertOverrides", x => new { x.SubscriptionId, x.GroupId }); + }); + + migrationBuilder.CreateIndex( + name: "IX_XchangeResults_RetryGroupId", + table: "XchangeResults", + column: "RetryGroupId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RetryAlertOverrides"); + + migrationBuilder.DropIndex( + name: "IX_XchangeResults_RetryGroupId", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "AttemptNumber", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "RetryGroupId", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "AlertHandlerId", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "AlertHandlerProperties", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "ExhaustedNotifiedOn", + table: "RetryGroupUsages"); + + // These rows are the alert's own delivery log, and they are the reason the column was + // made nullable. Rolling the feature back leaves nowhere to put them, and the column + // cannot go back to NOT NULL while they are here, so they go with the feature. + migrationBuilder.Sql( + "DELETE FROM [XchangeNotifications] WHERE [NotifierId] IS NULL;"); + + migrationBuilder.AlterColumn( + name: "NotifierId", + table: "XchangeNotifications", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.Designer.cs b/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.Designer.cs new file mode 100644 index 00000000..0db64634 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.Designer.cs @@ -0,0 +1,1959 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260819100503_ManualRetryFlag")] + partial class ManualRetryFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260819100503_ManualRetryFlag.cs b/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.cs new file mode 100644 index 00000000..73d83836 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class ManualRetryFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ManualRetry", + table: "Xchanges", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ManualRetry", + table: "Xchanges"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.Designer.cs b/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.Designer.cs new file mode 100644 index 00000000..7f28c1c5 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.Designer.cs @@ -0,0 +1,2104 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260823104233_GatewayInactiveFlag")] + partial class GatewayInactiveFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260823104233_GatewayInactiveFlag.cs b/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.cs new file mode 100644 index 00000000..129b1d34 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class GatewayInactiveFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Inactive", + table: "BusGateways", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Inactive", + table: "ApiGateways", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Inactive", + table: "BusGateways"); + + migrationBuilder.DropColumn( + name: "Inactive", + table: "ApiGateways"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.Designer.cs b/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.Designer.cs new file mode 100644 index 00000000..ef118cc7 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.Designer.cs @@ -0,0 +1,2138 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260824093631_AddReceiveAttempts")] + partial class AddReceiveAttempts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .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"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MsSql/Migrations/20260824093631_AddReceiveAttempts.cs b/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.cs new file mode 100644 index 00000000..044c3a9b --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddReceiveAttempts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ReceiveAttempts", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + SubscriptionId = table.Column(type: "int", nullable: false), + StartedOn = table.Column(type: "datetime2", nullable: false), + FinishedOn = table.Column(type: "datetime2", nullable: false), + Outcome = table.Column(type: "int", nullable: false), + ErrorMessage = table.Column(type: "nvarchar(4000)", maxLength: 4000, nullable: true), + ExchangeIds = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ReceiveAttempts", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ReceiveAttempts_SubscriptionId_StartedOn", + table: "ReceiveAttempts", + columns: new[] { "SubscriptionId", "StartedOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ReceiveAttempts"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs index 43e9d519..de43d8c9 100644 --- a/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -22,6 +22,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + modelBuilder.HasSequence("DocumentIds"); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => { b.Property("Id") @@ -55,6 +57,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EmailProvider") .HasColumnType("tinyint"); + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + b.Property("LoginMethods") .HasColumnType("tinyint"); @@ -69,11 +77,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(500)"); - b.Property("Phone") - .HasMaxLength(20) - .IsUnicode(false) - .HasColumnType("varchar(20)"); - b.Property("Role") .HasColumnType("int"); @@ -95,12 +98,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) DisplayName = "Admin", Email = "admin@Bitween.systems", EmailProvider = (byte)0, + FailedLoginCount = 0, LoginMethods = (byte)2, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", Role = 0 }); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => { b.Property("Id") @@ -124,6 +143,78 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RefreshTokens", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => { b.Property("Id") @@ -131,9 +222,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(50)"); - b.Property("GroupAttemptCounts") - .HasColumnType("nvarchar(max)"); - b.Property("On") .HasColumnType("datetime2"); @@ -147,7 +235,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("SW.Bitween.Domain.Document", b => { b.Property("Id") - .HasColumnType("int"); + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); b.Property("BusEnabled") .HasColumnType("bit"); @@ -157,6 +249,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(500)"); + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("DisregardsUnfilteredMessages") .HasColumnType("bit"); @@ -181,6 +278,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnique() .HasFilter("[BusMessageTypeName] IS NOT NULL"); + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + b.HasIndex("Name") .IsUnique(); @@ -245,6 +346,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedOn") .HasColumnType("datetime2"); + b.Property("Inactive") + .HasColumnType("bit"); + b.Property("ModifiedBy") .HasColumnType("nvarchar(max)"); @@ -318,6 +422,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); + b.Property("Inactive") + .HasColumnType("bit"); + b.Property("ModifiedBy") .HasColumnType("nvarchar(max)"); @@ -498,6 +605,86 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => { b.Property("Id") @@ -506,6 +693,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + b.Property("CreatedBy") .HasColumnType("nvarchar(max)"); @@ -531,6 +726,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RetryPolicies", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => { b.Property("Id") @@ -766,9 +988,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); - b.Property("GroupAttemptCounts") - .HasColumnType("nvarchar(max)"); - b.Property("HandlerId") .HasMaxLength(200) .IsUnicode(false) @@ -795,6 +1014,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InputSize") .HasColumnType("int"); + b.Property("ManualRetry") + .HasColumnType("bit"); + b.Property("MapperId") .HasMaxLength(200) .IsUnicode(false) @@ -898,7 +1120,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FinishedOn") .HasColumnType("datetime2"); - b.Property("NotifierId") + b.Property("NotifierId") .HasColumnType("int"); b.Property("NotifierName") @@ -949,6 +1171,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(50)"); + b.Property("AttemptNumber") + .HasColumnType("int"); + b.Property("Exception") .HasColumnType("nvarchar(max)"); @@ -998,11 +1223,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResponseXchangeId") .HasColumnType("nvarchar(max)"); + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + b.Property("Success") .HasColumnType("bit"); b.HasKey("Id"); + b.HasIndex("RetryGroupId"); + b.ToTable("XchangeResults", (string)null); }); @@ -1535,6 +1769,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("QRTZ_triggers", "dbo"); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => { b.HasOne("SW.Bitween.Domain.Accounts.Account", null) diff --git a/SW.Bitween.MsSql/SW.Bitween.MsSql.csproj b/SW.Bitween.MsSql/SW.Bitween.MsSql.csproj index bb41ca29..bf26e8a3 100644 --- a/SW.Bitween.MsSql/SW.Bitween.MsSql.csproj +++ b/SW.Bitween.MsSql/SW.Bitween.MsSql.csproj @@ -1,13 +1,13 @@ - net8.0 + net10.0 SW.Bitween.MsSql - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.Designer.cs b/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.Designer.cs new file mode 100644 index 00000000..991b2d09 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.Designer.cs @@ -0,0 +1,1897 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260726123628_AddDocumentCodeAndAutoId")] + partial class AddDocumentCodeAndAutoId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.cs b/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.cs new file mode 100644 index 00000000..c32df956 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddDocumentCodeAndAutoId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Id", + table: "Documents", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AddColumn( + name: "Code", + table: "Documents", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "Documents", + keyColumn: "Id", + keyValue: 10001, + column: "Code", + value: null); + + migrationBuilder.CreateIndex( + name: "IX_Documents_Code", + table: "Documents", + column: "Code", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Documents_Code", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "Code", + table: "Documents"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Documents", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.Designer.cs b/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.Designer.cs new file mode 100644 index 00000000..9c4272f0 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.Designer.cs @@ -0,0 +1,1999 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260726123824_AddRolesAndPermissions")] + partial class AddRolesAndPermissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260726123824_AddRolesAndPermissions.cs b/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.cs new file mode 100644 index 00000000..7b5ba487 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.cs @@ -0,0 +1,112 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddRolesAndPermissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "varchar(500)", maxLength: 500, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Permissions = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + IsSystem = table.Column(type: "tinyint(1)", nullable: false), + CreatedOn = table.Column(type: "datetime(6)", nullable: false), + CreatedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ModifiedOn = table.Column(type: "datetime(6)", nullable: true), + ModifiedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AccountRoles", + columns: table => new + { + AccountId = table.Column(type: "int", nullable: false), + RoleId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountRoles", x => new { x.AccountId, x.RoleId }); + table.ForeignKey( + name: "FK_AccountRoles_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AccountRoles_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.InsertData( + table: "Roles", + columns: new[] { "Id", "CreatedBy", "CreatedOn", "Description", "IsSystem", "ModifiedBy", "ModifiedOn", "Name", "Permissions" }, + values: new object[,] + { + { 1, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Full access to everything, including members, roles and settings.", true, null, null, "Administrator", "[]" }, + { 2, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Runs and configures integrations. Can't manage members, roles or settings.", true, null, null, "Member", "[]" }, + { 3, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Read-only access to integrations, exchanges and configuration.", true, null, null, "Viewer", "[]" } + }); + + migrationBuilder.CreateIndex( + name: "IX_AccountRoles_RoleId", + table: "AccountRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_Roles_Name", + table: "Roles", + column: "Name", + unique: true); + + // Every existing account keeps exactly the access it had: its coarse AccountRole + // (Admin=0, Viewer=10, Member=20) becomes the matching built-in role. Anything + // unrecognised lands on Viewer — least privilege. Idempotent, so re-running or + // ordering against the seeded admin account can't produce duplicates. + migrationBuilder.Sql(""" + INSERT INTO `AccountRoles` (`AccountId`, `RoleId`) + SELECT a.`Id`, CASE a.`Role` WHEN 0 THEN 1 WHEN 20 THEN 2 ELSE 3 END + FROM `Accounts` a + WHERE NOT EXISTS ( + SELECT 1 FROM `AccountRoles` l WHERE l.`AccountId` = a.`Id`); + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccountRoles"); + + migrationBuilder.DropTable( + name: "Roles"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260726151533_DropAccountPhone.Designer.cs b/SW.Bitween.MySql/Migrations/20260726151533_DropAccountPhone.Designer.cs new file mode 100644 index 00000000..4102d5ff --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260726151533_DropAccountPhone.Designer.cs @@ -0,0 +1,1994 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260726151533_DropAccountPhone")] + partial class DropAccountPhone + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260726151533_DropAccountPhone.cs b/SW.Bitween.MySql/Migrations/20260726151533_DropAccountPhone.cs new file mode 100644 index 00000000..e19d8877 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260726151533_DropAccountPhone.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class DropAccountPhone : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Phone", + table: "Accounts"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Phone", + table: "Accounts", + type: "varchar(20)", + unicode: false, + maxLength: 20, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + column: "Phone", + value: null); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260727133053_AddSettings.Designer.cs b/SW.Bitween.MySql/Migrations/20260727133053_AddSettings.Designer.cs new file mode 100644 index 00000000..e61dd77f --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260727133053_AddSettings.Designer.cs @@ -0,0 +1,2021 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260727133053_AddSettings")] + partial class AddSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260727133053_AddSettings.cs b/SW.Bitween.MySql/Migrations/20260727133053_AddSettings.cs new file mode 100644 index 00000000..b93fdb8a --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260727133053_AddSettings.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Settings", + columns: table => new + { + Id = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Value = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedOn = table.Column(type: "datetime(6)", nullable: false), + CreatedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ModifiedOn = table.Column(type: "datetime(6)", nullable: true), + ModifiedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_Settings", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Settings"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.Designer.cs b/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.Designer.cs new file mode 100644 index 00000000..3ed0e858 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.Designer.cs @@ -0,0 +1,1899 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811081221_SharedRetryGroupTotals")] + partial class SharedRetryGroupTotals + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs b/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs new file mode 100644 index 00000000..3bda1da3 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class SharedRetryGroupTotals : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "Xchanges"); + + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "DelayedRetries"); + + migrationBuilder.CreateTable( + name: "RetryGroupUsages", + columns: table => new + { + SubscriptionId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + AttemptsUsed = table.Column(type: "int", nullable: false), + LastAttemptOn = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RetryGroupUsages", x => new { x.SubscriptionId, x.GroupId }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RetryGroupUsages"); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "Xchanges", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "DelayedRetries", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.Designer.cs b/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.Designer.cs new file mode 100644 index 00000000..53cb83ff --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.Designer.cs @@ -0,0 +1,1903 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811092323_RetryBlockedReason")] + partial class RetryBlockedReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260811092323_RetryBlockedReason.cs b/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.cs new file mode 100644 index 00000000..81d72732 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class RetryBlockedReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RetryBlockedReason", + table: "XchangeResults", + type: "varchar(500)", + maxLength: 500, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RetryBlockedReason", + table: "XchangeResults"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.Designer.cs b/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.Designer.cs new file mode 100644 index 00000000..b7948513 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.Designer.cs @@ -0,0 +1,1893 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260812092701_AddAccountLockout")] + partial class AddAccountLockout + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260812092701_AddAccountLockout.cs b/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.cs new file mode 100644 index 00000000..0fabaec5 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddAccountLockout : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FailedLoginCount", + table: "Accounts", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "LockoutEnd", + table: "Accounts", + type: "datetime(6)", + nullable: true); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + columns: new[] { "FailedLoginCount", "LockoutEnd" }, + values: new object[] { 0, null }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FailedLoginCount", + table: "Accounts"); + + migrationBuilder.DropColumn( + name: "LockoutEnd", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.Designer.cs b/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.Designer.cs new file mode 100644 index 00000000..087fa89c --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.Designer.cs @@ -0,0 +1,1953 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260817103452_RetryBudgetAlerts")] + partial class RetryBudgetAlerts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260817103452_RetryBudgetAlerts.cs b/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.cs new file mode 100644 index 00000000..278a5f02 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.cs @@ -0,0 +1,128 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class RetryBudgetAlerts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AttemptNumber", + table: "XchangeResults", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "RetryGroupId", + table: "XchangeResults", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AlterColumn( + name: "NotifierId", + table: "XchangeNotifications", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AddColumn( + name: "AlertHandlerId", + table: "RetryPolicies", + type: "varchar(200)", + unicode: false, + maxLength: 200, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "AlertHandlerProperties", + table: "RetryPolicies", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "ExhaustedNotifiedOn", + table: "RetryGroupUsages", + type: "datetime(6)", + nullable: true); + + migrationBuilder.CreateTable( + name: "RetryAlertOverrides", + columns: table => new + { + SubscriptionId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + AlertMode = table.Column(type: "tinyint unsigned", nullable: false), + AlertHandlerId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + AlertHandlerProperties = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_RetryAlertOverrides", x => new { x.SubscriptionId, x.GroupId }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_XchangeResults_RetryGroupId", + table: "XchangeResults", + column: "RetryGroupId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RetryAlertOverrides"); + + migrationBuilder.DropIndex( + name: "IX_XchangeResults_RetryGroupId", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "AttemptNumber", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "RetryGroupId", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "AlertHandlerId", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "AlertHandlerProperties", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "ExhaustedNotifiedOn", + table: "RetryGroupUsages"); + + // These rows are the alert's own delivery log, and they are the reason the column was + // made nullable. Rolling the feature back leaves nowhere to put them, and the column + // cannot go back to NOT NULL while they are here, so they go with the feature. + migrationBuilder.Sql( + "DELETE FROM `XchangeNotifications` WHERE `NotifierId` IS NULL;"); + + migrationBuilder.AlterColumn( + name: "NotifierId", + table: "XchangeNotifications", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.Designer.cs b/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.Designer.cs new file mode 100644 index 00000000..61ddbe0f --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.Designer.cs @@ -0,0 +1,1956 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260819100452_ManualRetryFlag")] + partial class ManualRetryFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260819100452_ManualRetryFlag.cs b/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.cs new file mode 100644 index 00000000..2be75b27 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class ManualRetryFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ManualRetry", + table: "Xchanges", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ManualRetry", + table: "Xchanges"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.Designer.cs b/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.Designer.cs new file mode 100644 index 00000000..217041ea --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.Designer.cs @@ -0,0 +1,2097 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260823104220_GatewayInactiveFlag")] + partial class GatewayInactiveFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260823104220_GatewayInactiveFlag.cs b/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.cs new file mode 100644 index 00000000..30574d06 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class GatewayInactiveFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Inactive", + table: "BusGateways", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Inactive", + table: "ApiGateways", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Inactive", + table: "BusGateways"); + + migrationBuilder.DropColumn( + name: "Inactive", + table: "ApiGateways"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.Designer.cs b/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.Designer.cs new file mode 100644 index 00000000..e8d70e04 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.Designer.cs @@ -0,0 +1,2131 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260824093618_AddReceiveAttempts")] + partial class AddReceiveAttempts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + 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", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .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("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .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("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .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"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .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("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + 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"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + 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_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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.MySql/Migrations/20260824093618_AddReceiveAttempts.cs b/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.cs new file mode 100644 index 00000000..9402ea68 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddReceiveAttempts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ReceiveAttempts", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + SubscriptionId = table.Column(type: "int", nullable: false), + StartedOn = table.Column(type: "datetime(6)", nullable: false), + FinishedOn = table.Column(type: "datetime(6)", nullable: false), + Outcome = table.Column(type: "int", nullable: false), + ErrorMessage = table.Column(type: "varchar(4000)", maxLength: 4000, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ExchangeIds = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ReceiveAttempts", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ReceiveAttempts_SubscriptionId_StartedOn", + table: "ReceiveAttempts", + columns: new[] { "SubscriptionId", "StartedOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ReceiveAttempts"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs index ee14e415..78a9a898 100644 --- a/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs @@ -55,6 +55,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EmailProvider") .HasColumnType("tinyint unsigned"); + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + b.Property("LoginMethods") .HasColumnType("tinyint unsigned"); @@ -69,11 +75,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(500)"); - b.Property("Phone") - .HasMaxLength(20) - .IsUnicode(false) - .HasColumnType("varchar(20)"); - b.Property("Role") .HasColumnType("int"); @@ -94,12 +95,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) DisplayName = "Admin", Email = "admin@Bitween.systems", EmailProvider = (byte)0, + FailedLoginCount = 0, LoginMethods = (byte)2, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", Role = 0 }); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => { b.Property("Id") @@ -123,6 +140,78 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RefreshTokens", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => { b.Property("Id") @@ -130,9 +219,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(50)"); - b.Property("GroupAttemptCounts") - .HasColumnType("longtext"); - b.Property("On") .HasColumnType("datetime(6)"); @@ -146,8 +232,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("SW.Bitween.Domain.Document", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("BusEnabled") .HasColumnType("tinyint(1)"); @@ -156,6 +245,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(500)"); + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("DisregardsUnfilteredMessages") .HasColumnType("tinyint(1)"); @@ -179,6 +273,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("BusMessageTypeName") .IsUnique(); + b.HasIndex("Code") + .IsUnique(); + b.HasIndex("Name") .IsUnique(); @@ -243,6 +340,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedOn") .HasColumnType("datetime(6)"); + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + b.Property("ModifiedBy") .HasColumnType("longtext"); @@ -316,6 +416,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + b.Property("ModifiedBy") .HasColumnType("longtext"); @@ -496,6 +599,86 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => { b.Property("Id") @@ -504,6 +687,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + b.Property("CreatedBy") .HasColumnType("longtext"); @@ -529,6 +720,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RetryPolicies", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => { b.Property("Id") @@ -763,9 +981,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); - b.Property("GroupAttemptCounts") - .HasColumnType("longtext"); - b.Property("HandlerId") .HasMaxLength(200) .IsUnicode(false) @@ -792,6 +1007,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InputSize") .HasColumnType("int"); + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + b.Property("MapperId") .HasMaxLength(200) .IsUnicode(false) @@ -895,7 +1113,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FinishedOn") .HasColumnType("datetime(6)"); - b.Property("NotifierId") + b.Property("NotifierId") .HasColumnType("int"); b.Property("NotifierName") @@ -946,6 +1164,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(50)"); + b.Property("AttemptNumber") + .HasColumnType("int"); + b.Property("Exception") .HasColumnType("longtext"); @@ -995,11 +1216,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResponseXchangeId") .HasColumnType("longtext"); + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + b.Property("Success") .HasColumnType("tinyint(1)"); b.HasKey("Id"); + b.HasIndex("RetryGroupId"); + b.ToTable("XchangeResults", (string)null); }); @@ -1532,6 +1762,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("QRTZ_triggers", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => { b.HasOne("SW.Bitween.Domain.Accounts.Account", null) diff --git a/SW.Bitween.MySql/SW.Bitween.MySql.csproj b/SW.Bitween.MySql/SW.Bitween.MySql.csproj index 3081064c..e34a0540 100644 --- a/SW.Bitween.MySql/SW.Bitween.MySql.csproj +++ b/SW.Bitween.MySql/SW.Bitween.MySql.csproj @@ -1,13 +1,13 @@ - net8.0 + net10.0 SW.Bitween.MySql - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/SW.Bitween.NativeAdapters/Interfaces.cs b/SW.Bitween.NativeAdapters/Interfaces.cs index b03f2083..a204ce5b 100644 --- a/SW.Bitween.NativeAdapters/Interfaces.cs +++ b/SW.Bitween.NativeAdapters/Interfaces.cs @@ -9,6 +9,13 @@ public interface INativeAdapter public Type StartupValuesType { get; } } +/// +/// Marks an adapter that only works with a Rebex license key configured. Such adapters are +/// always registered — the key is a setting that can change at runtime — but they're kept out +/// of the adapter pickers while no key is set. +/// +public interface IRequiresRebexLicense { } + public interface INativeInfolinkHandler : INativeAdapter, IInfolinkHandler { } public interface INativeInfolinkMapper : INativeAdapter, IInfolinkHandler { } public interface INativeInfolinkValidator : IInfolinkValidator, INativeAdapter { } diff --git a/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs b/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs index eb369695..b535a8db 100644 --- a/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs +++ b/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs @@ -13,6 +13,37 @@ public static class ScribanJsonHelper /// Renders a Scriban template against the provided input JSON and returns the mapped output JSON. /// ` public static string Render(string scribanTemplate, string inputJson) + { + var rendered = RenderText(scribanTemplate, inputJson); + + // 6. Strip trailing commas that may appear after the last field/element + rendered = Regex.Replace(rendered, @",(\s*[}\]])", "$1"); + + // 7. Parse rendered output — root may be an object OR an array + JToken renderedToken; + try + { + renderedToken = JToken.Parse(rendered); + } + catch (JsonException ex) + { + throw new InvalidOperationException($"Template produced invalid JSON: {ex.Message}\n\nRendered:\n{rendered}"); + } + + // 8. Expand dotted keys into nested objects recursively at all depths + return ExpandDottedKeys(renderedToken).ToString(Formatting.Indented); + } + + /// + /// Renders a Scriban template against the provided input JSON and returns the text as-is, + /// without requiring the result to be JSON. + /// + /// + /// For templates whose output is prose rather than a payload — an email subject or body, say. + /// builds on this and adds the JSON validation and dotted-key expansion + /// that a mapper needs and a sentence does not. + /// + public static string RenderText(string scribanTemplate, string inputJson) { // 1. Parse input JSON — handle both root object and root array var rootToken = JToken.Parse(inputJson); @@ -71,24 +102,7 @@ public static string Render(string scribanTemplate, string inputJson) throw new InvalidOperationException($"Template parse error: {errors}"); } - var rendered = template.Render(context); - - // 6. Strip trailing commas that may appear after the last field/element - rendered = Regex.Replace(rendered, @",(\s*[}\]])", "$1"); - - // 7. Parse rendered output — root may be an object OR an array - JToken renderedToken; - try - { - renderedToken = JToken.Parse(rendered); - } - catch (JsonException ex) - { - throw new InvalidOperationException($"Template produced invalid JSON: {ex.Message}\n\nRendered:\n{rendered}"); - } - - // 8. Expand dotted keys into nested objects recursively at all depths - return ExpandDottedKeys(renderedToken).ToString(Formatting.Indented); + return template.Render(context); } // ─── Helpers ────────────────────────────────────────────────────────────── diff --git a/SW.Bitween.NativeAdapters/RebexFtpReceiver/NativeRebexFtpReceiver.cs b/SW.Bitween.NativeAdapters/RebexFtpReceiver/NativeRebexFtpReceiver.cs index 18e8335b..65dadebf 100644 --- a/SW.Bitween.NativeAdapters/RebexFtpReceiver/NativeRebexFtpReceiver.cs +++ b/SW.Bitween.NativeAdapters/RebexFtpReceiver/NativeRebexFtpReceiver.cs @@ -4,7 +4,7 @@ namespace SW.Bitween.NativeAdapters.RebexFtpReceiver; -public class NativeRebexFtpReceiver : INativeInfolinkReceiver +public class NativeRebexFtpReceiver : INativeInfolinkReceiver, IRequiresRebexLicense { private readonly string? _licenseKey; private RebexFtpReceiverInput _options = new(); diff --git a/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/NativeRebexFtpUploadHandler.cs b/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/NativeRebexFtpUploadHandler.cs index bee8c876..7041bf85 100644 --- a/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/NativeRebexFtpUploadHandler.cs +++ b/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/NativeRebexFtpUploadHandler.cs @@ -4,7 +4,7 @@ namespace SW.Bitween.NativeAdapters.RebexFtpUploadHandler; -public class NativeRebexFtpUploadHandler : INativeInfolinkHandler +public class NativeRebexFtpUploadHandler : INativeInfolinkHandler, IRequiresRebexLicense { private readonly string? _licenseKey; private RebexFtpUploadHandlerInput _options = new(); diff --git a/SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs b/SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs index 7ef949f7..328de326 100644 --- a/SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs +++ b/SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs @@ -4,7 +4,7 @@ namespace SW.Bitween.NativeAdapters.RebexPop3Receiver; -public class NativeRebexPop3Receiver : INativeInfolinkReceiver +public class NativeRebexPop3Receiver : INativeInfolinkReceiver, IRequiresRebexLicense { private readonly string? _licenseKey; diff --git a/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj b/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj index 3bbdcaba..007c67b1 100644 --- a/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj +++ b/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs b/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs index 5f0c882a..2202564e 100644 --- a/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs +++ b/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs @@ -8,12 +8,19 @@ using SW.Bitween.NativeAdapters.RebexPop3Receiver; using SW.Bitween.NativeAdapters.S3Receiver; using SW.Bitween.NativeAdapters.S3UploadHandler; +using SW.Bitween.NativeAdapters.SmtpHandler; namespace SW.Bitween.NativeAdapters; public static class ServiceCollectionExtensions { - public static void AddNativeAdapters(this IServiceCollection serviceCollection, string? rebexLicenseKey = null) + /// + /// Reads the current Rebex license key. Called per adapter instance rather than once here, + /// because the key is a setting that can be changed while the app is running — pasting one in + /// makes the Rebex adapters usable without a restart. + /// + public static void AddNativeAdapters(this IServiceCollection serviceCollection, + Func? rebexLicenseKey = null) { serviceCollection.ConfigureHttpClientDefaults(builder => { @@ -42,6 +49,9 @@ public static void AddNativeAdapters(this IServiceCollection serviceCollection, serviceCollection.AddScoped(); serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); serviceCollection.AddScoped(); @@ -54,16 +64,16 @@ public static void AddNativeAdapters(this IServiceCollection serviceCollection, serviceCollection.AddScoped(); serviceCollection.AddScoped(); - if (!string.IsNullOrEmpty(rebexLicenseKey)) + if (rebexLicenseKey is not null) { - serviceCollection.AddScoped(_ => new NativeRebexPop3Receiver(rebexLicenseKey)); - serviceCollection.AddScoped(_ => new NativeRebexPop3Receiver(rebexLicenseKey)); + serviceCollection.AddScoped(sp => new NativeRebexPop3Receiver(rebexLicenseKey(sp))); + serviceCollection.AddScoped(sp => new NativeRebexPop3Receiver(rebexLicenseKey(sp))); - serviceCollection.AddScoped(_ => new NativeRebexFtpUploadHandler(rebexLicenseKey)); - serviceCollection.AddScoped(_ => new NativeRebexFtpUploadHandler(rebexLicenseKey)); + serviceCollection.AddScoped(sp => new NativeRebexFtpUploadHandler(rebexLicenseKey(sp))); + serviceCollection.AddScoped(sp => new NativeRebexFtpUploadHandler(rebexLicenseKey(sp))); - serviceCollection.AddScoped(_ => new NativeRebexFtpReceiver(rebexLicenseKey)); - serviceCollection.AddScoped(_ => new NativeRebexFtpReceiver(rebexLicenseKey)); + serviceCollection.AddScoped(sp => new NativeRebexFtpReceiver(rebexLicenseKey(sp))); + serviceCollection.AddScoped(sp => new NativeRebexFtpReceiver(rebexLicenseKey(sp))); } } } \ No newline at end of file diff --git a/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs b/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs new file mode 100644 index 00000000..6cc74e84 --- /dev/null +++ b/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs @@ -0,0 +1,165 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using MailKit.Net.Smtp; +using MailKit.Security; +using MimeKit; +using MimeKit.Text; +using Newtonsoft.Json.Linq; +using SW.Bitween.NativeAdapters.JsonMapper; +using SW.PrimitiveTypes; + +namespace SW.Bitween.NativeAdapters.SmtpHandler; + +/// +/// Sends the payload as an email, with the subject and body written as templates over it. +/// +/// +/// Built for cases where the recipient is a person rather than a system — a retry budget running +/// out, a notifier on a failed exchange — which is why the subject and body are templated instead +/// of the payload being emailed raw. A JSON blob in an inbox tells nobody anything. +/// +public class NativeSmtpHandler : INativeInfolinkHandler +{ + private SmtpHandlerInput _options = new(); + + public string Name => "NativeSmtpHandler"; + + public Type StartupValuesType => typeof(SmtpHandlerInput); + + public void InitializeStartupValues(IDictionary settings) + { + _options = settings.ConvertTo(); + } + + public async Task Handle(XchangeFile xchangeFile) + { + var subject = Fill(_options.Subject, xchangeFile.Data); + var body = Fill(_options.Body, xchangeFile.Data); + + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(_options.FromName ?? string.Empty, _options.From)); + message.Subject = subject; + message.Body = new TextPart(_options.IsHtml ? TextFormat.Html : TextFormat.Plain) { Text = body }; + + AddAddresses(message.To, _options.To); + AddAddresses(message.Cc, _options.Cc); + AddAddresses(message.Bcc, _options.Bcc); + + if (message.To.Count == 0 && message.Cc.Count == 0 && message.Bcc.Count == 0) + throw new InvalidOperationException("No recipients were configured for the SMTP handler."); + + using var client = new SmtpClient(); + + // Revocation stays switched on, so a certificate the CA has actually revoked is still refused. + // What the callback softens is the other outcome: the lookup needs the CA's OCSP or CRL server + // to be reachable, which the corporate networks Bitween runs inside routinely block, and + // MailKit's default treats "could not find out" exactly like "revoked". That rejected a + // perfectly good Gmail certificate — one OpenSSL accepts on the same machine — and the alert + // never went out. Soft-failing an undeterminable status is what browsers and mail clients do; + // every other defect, revocation included, still fails. + client.CheckCertificateRevocation = true; + client.ServerCertificateValidationCallback = (_, _, chain, errors) => + IsCertificateAcceptable(errors, + chain?.ChainStatus.Select(s => s.Status) ?? Enumerable.Empty()); + + // Named rather than left to Auto: on any port but 465, Auto means "encrypt if the server + // offers it", so a server that does not offer STARTTLS — or an offer stripped in transit — + // silently continues in the clear. StartTls demands it and fails if it is not there. 465 is + // the implicit-TLS port, where the handshake happens before any of that is negotiable. + var security = _options.UseTls + ? _options.Port == 465 ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTls + : SecureSocketOptions.None; + + await client.ConnectAsync(_options.Host, _options.Port, security); + + // A relay that accepts unauthenticated mail from inside the network is a normal setup, so + // only authenticate when a password was actually supplied. + if (!string.IsNullOrWhiteSpace(_options.Password)) + { + // Refusing beats sending the credential over a connection anyone on the path can read. + if (!client.IsSecure) + throw new InvalidOperationException( + "The SMTP handler will not send a password over an unencrypted connection. " + + "Set UseTls to true, or clear the password if the relay does not need one."); + + await client.AuthenticateAsync( + string.IsNullOrWhiteSpace(_options.Username) ? _options.From : _options.Username, + _options.Password); + } + + await client.SendAsync(message); + await client.DisconnectAsync(true); + + return new XchangeFile(subject, xchangeFile.Filename); + } + + private static void AddAddresses(InternetAddressList list, string? addresses) + { + if (string.IsNullOrWhiteSpace(addresses)) return; + + foreach (var address in addresses.Split(',', StringSplitOptions.RemoveEmptyEntries + | StringSplitOptions.TrimEntries)) + list.Add(MailboxAddress.Parse(address)); + } + + /// + /// Renders a template against the payload, or returns it unchanged when the payload is not JSON. + /// + /// + /// Non-JSON payloads are normal for a pipeline handler — a CSV or a flat file on its way out — + /// and those have no fields to substitute. A broken template still throws, so a typo in a + /// placeholder is not quietly emailed as literal text. + /// + internal static string Fill(string template, string payload) + { + if (string.IsNullOrEmpty(template) || !LooksLikeJson(payload)) return template; + + return ScribanJsonHelper.RenderText(template, payload); + } + + /// + /// Whether a server certificate should be accepted, given what validation found wrong with it. + /// + /// + /// Only one defect is tolerated: a revocation status that could not be established, because the + /// CA's OCSP or CRL server was unreachable. A certificate the CA has revoked, an untrusted root, + /// a wrong hostname and an expired certificate are all still refused — as is any chain flag not + /// named here, so a defect nobody thought about fails closed rather than slipping through. + /// + internal static bool IsCertificateAcceptable(SslPolicyErrors errors, + IEnumerable chainStatus) + { + if (errors == SslPolicyErrors.None) return true; + + // A missing certificate or the wrong name on one is not a revocation question at all, and the + // chain flags say nothing about either. + if (errors != SslPolicyErrors.RemoteCertificateChainErrors) return false; + + const X509ChainStatusFlags undeterminable = + X509ChainStatusFlags.RevocationStatusUnknown | X509ChainStatusFlags.OfflineRevocation; + + // Masked rather than compared: one entry can carry several flags at once, and "revoked" set + // alongside "could not check" has to fail. + return chainStatus.All(status => (status & ~undeterminable) == X509ChainStatusFlags.NoError); + } + + private static bool LooksLikeJson(string payload) + { + if (string.IsNullOrWhiteSpace(payload)) return false; + + var trimmed = payload.TrimStart(); + if (trimmed[0] is not ('{' or '[')) return false; + + try + { + JToken.Parse(payload); + return true; + } + catch + { + return false; + } + } +} diff --git a/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs b/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs new file mode 100644 index 00000000..b190827b --- /dev/null +++ b/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs @@ -0,0 +1,59 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace SW.Bitween.NativeAdapters.SmtpHandler; + +public class SmtpHandlerInput +{ + [Required] + [Description("SMTP server hostname.")] + public string Host { get; set; } = string.Empty; + + [DefaultValue(587)] + [Description("SMTP server port. 587 for STARTTLS, 465 for implicit SSL, 25 for an unencrypted relay.")] + public int Port { get; set; } = 587; + + [Description("SMTP username. Leave empty to authenticate as the From address, or for a relay that needs no credentials.")] + public string? Username { get; set; } + + [Secure] + [Description("SMTP password. Leave empty for a relay that needs no credentials.")] + public string? Password { get; set; } + + [DefaultValue(true)] + [Description("Encrypt the connection, choosing STARTTLS or SSL to match the port. Turn off only for an internal relay with no TLS.")] + public bool UseTls { get; set; } = true; + + [Required] + [Description("Address the message is sent from.")] + public string From { get; set; } = string.Empty; + + [Description("Display name shown beside the From address, e.g. Bitween Alerts.")] + public string? FromName { get; set; } + + [Required] + [Description("Recipients, separated by commas.")] + public string To { get; set; } = string.Empty; + + [Description("Carbon-copy recipients, separated by commas.")] + public string? Cc { get; set; } + + [Description("Blind carbon-copy recipients, separated by commas.")] + public string? Bcc { get; set; } + + [Required] + [Description( + "Subject line. Placeholders in the incoming payload are substituted, e.g. " + + "'Retries stopped for {{ SubscriptionName }}'.")] + public string Subject { get; set; } = string.Empty; + + [Required] + [Description( + "Message body. Uses the same template syntax as the JSON mapper, so payload fields can be " + + "referenced directly, e.g. '{{ GroupName }} used all {{ MaxAttemptsTotal }} retries.'")] + public string Body { get; set; } = string.Empty; + + [DefaultValue(true)] + [Description("Send the body as HTML. Turn off to send it as plain text.")] + public bool IsHtml { get; set; } = true; +} diff --git a/SW.Bitween.PgSql/BitweenDbContext.cs b/SW.Bitween.PgSql/BitweenDbContext.cs index b159f6ce..b0456c75 100644 --- a/SW.Bitween.PgSql/BitweenDbContext.cs +++ b/SW.Bitween.PgSql/BitweenDbContext.cs @@ -41,12 +41,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(b => { //b.ToTable("Documents"); - b.Property(p => p.Id).ValueGeneratedNever(); b.Property(p => p.Name).HasMaxLength(100).IsRequired(); + b.Property(p => p.Code).HasMaxLength(50); b.Property(p => p.BusMessageTypeName).HasMaxLength(500); b.Property(p => p.PromotedProperties).StoreAsJson().HasColumnType("jsonb"); b.Property(p => p.DisregardsUnfilteredMessages).IsRequired(false); b.HasIndex(p => p.Name).IsUnique(); + b.HasIndex(p => p.Code).IsUnique(); b.HasIndex(p => p.BusMessageTypeName).IsUnique(); b.HasMany().WithOne().HasForeignKey(p => p.DocumentId).OnDelete(DeleteBehavior.Restrict); @@ -262,6 +263,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.ResponseName).HasMaxLength(200); b.Property(p => p.ResponseContentType).HasMaxLength(200); b.Property(p => p.OutputContentType).HasMaxLength(200); + b.Property(p => p.RetryBlockedReason).HasMaxLength(500); + b.Property(p => p.RetryGroupId); + b.Property(p => p.AttemptNumber); + b.HasIndex(p => p.RetryGroupId); b.HasOne().WithOne().HasForeignKey(p => p.Id).OnDelete(DeleteBehavior.Cascade); }); @@ -320,7 +325,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasIndex(p => p.Email).IsUnique(); b.Property(p => p.Email).IsUnicode(false).HasMaxLength(200); - b.Property(p => p.Phone).IsUnicode(false).HasMaxLength(20); b.Property(p => p.Password).IsUnicode(false).HasMaxLength(500); b.Property(p => p.DisplayName).IsRequired().HasMaxLength(200); @@ -339,7 +343,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) CreatedOn = defaultCreatedOn.ToUniversalTime(), Disabled = false, Password = defaultPasswordHash, - Role = AccountRole.Admin + Role = AccountRole.Admin, + FailedLoginCount = 0 }); }); @@ -354,11 +359,44 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.LoginMethod).HasConversion(); }); + modelBuilder.Entity(b => + { + b.ToTable("Roles"); + b.HasKey(p => p.Id); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.HasIndex(p => p.Name).IsUnique(); + + b.Property(p => p.Name).IsRequired().HasMaxLength(100); + b.Property(p => p.Description).HasMaxLength(500); + b.Property(p => p.Permissions).StoreAsJson(); + + b.HasData(SystemRoleSeed()); + }); + + modelBuilder.Entity(b => + { + b.ToTable("AccountRoles"); + b.HasKey(p => new { p.AccountId, p.RoleId }); + b.HasOne().WithMany().HasForeignKey(p => p.AccountId).OnDelete(DeleteBehavior.Cascade); + b.HasOne().WithMany().HasForeignKey(p => p.RoleId).OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(b => + { + b.ToTable("Settings"); + b.HasKey(p => p.Id); + // Id is the catalog key, e.g. "Theme.PrimaryColor". Value is left unbounded: + // it carries anything from a hex color to a license key or a page of blurb. + b.Property(p => p.Id).IsUnicode(false).HasMaxLength(200); + }); + modelBuilder.Entity(b => { b.HasKey(p => p.Id); b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(p => p.Name).IsRequired().HasMaxLength(200); + b.Property(p => p.AlertHandlerId).HasMaxLength(200); + b.Property(p => p.AlertHandlerProperties).StoreAsJson(); b.Property(p => p.Groups).HasConversion( groups => JsonSerializer.Serialize(groups, _polymorphicOpts), json => JsonSerializer.Deserialize>(json, _polymorphicOpts)!, @@ -384,13 +422,29 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { b.HasKey(p => p.Id); b.Property(p => p.Id).HasMaxLength(50); - b.Property(p => p.GroupAttemptCounts).HasColumnType("jsonb"); b.HasIndex(p => p.On); }); - modelBuilder.Entity(b => + modelBuilder.Entity(b => + { + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.HasIndex(p => new { p.SubscriptionId, p.StartedOn }); + }); + + modelBuilder.Entity(b => + { + b.HasKey(p => new { p.SubscriptionId, p.GroupId }); + b.Property(p => p.AttemptsUsed); + b.Property(p => p.LastAttemptOn); + b.Property(p => p.ExhaustedNotifiedOn); + }); + + modelBuilder.Entity(b => { - b.Property(p => p.GroupAttemptCounts).HasColumnType("jsonb"); + b.HasKey(p => new { p.SubscriptionId, p.GroupId }); + b.Property(p => p.AlertMode).HasConversion(); + b.Property(p => p.AlertHandlerId).HasMaxLength(200); + b.Property(p => p.AlertHandlerProperties).StoreAsJson(); }); modelBuilder.UseSchedulerPostgreSql(Schema); diff --git a/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.Designer.cs b/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.Designer.cs new file mode 100644 index 00000000..77f9ae78 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.Designer.cs @@ -0,0 +1,2171 @@ +// +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("20260719141125_AddDocumentCodeAndAutoId")] + partial class AddDocumentCodeAndAutoId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + 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("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + 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, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", 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("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.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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + 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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260719141125_AddDocumentCodeAndAutoId.cs b/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.cs new file mode 100644 index 00000000..7c5b10da --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.cs @@ -0,0 +1,82 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddDocumentCodeAndAutoId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "id", + schema: "infolink", + table: "document", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + migrationBuilder.AddColumn( + name: "code", + schema: "infolink", + table: "document", + type: "character varying(50)", + maxLength: 50, + nullable: true); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "document", + keyColumn: "id", + keyValue: 10001, + column: "code", + value: null); + + // Code is optional — existing rows stay code = NULL rather than being + // backfilled from name, which risked duplicate-derived codes colliding + // against the unique index on environments with less controlled data. + migrationBuilder.CreateIndex( + name: "ix_document_code", + schema: "infolink", + table: "document", + column: "code", + unique: true); + + // The identity sequence starts at 1; realign it past the existing (user-assigned, + // pre-identity) ids so the next INSERT doesn't eventually collide with them. + migrationBuilder.Sql(@" + SELECT setval( + pg_get_serial_sequence('infolink.document', 'id'), + (SELECT COALESCE(MAX(id), 0) FROM infolink.document));"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_document_code", + schema: "infolink", + table: "document"); + + migrationBuilder.DropColumn( + name: "code", + schema: "infolink", + table: "document"); + + migrationBuilder.AlterColumn( + name: "id", + schema: "infolink", + table: "document", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer") + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.Designer.cs b/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.Designer.cs new file mode 100644 index 00000000..8032a518 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.Designer.cs @@ -0,0 +1,2290 @@ +// +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("20260726105636_AddRolesAndPermissions")] + partial class AddRolesAndPermissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + 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("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + 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, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", 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("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.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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + 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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260726105636_AddRolesAndPermissions.cs b/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.cs new file mode 100644 index 00000000..dd4ea6fa --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.cs @@ -0,0 +1,114 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddRolesAndPermissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Roles", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + permissions = table.Column(type: "text", nullable: true), + is_system = table.Column(type: "boolean", nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + created_by = table.Column(type: "text", nullable: true), + modified_on = table.Column(type: "timestamp with time zone", nullable: true), + modified_by = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_roles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "AccountRoles", + schema: "infolink", + columns: table => new + { + account_id = table.Column(type: "integer", nullable: false), + role_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_account_roles", x => new { x.account_id, x.role_id }); + table.ForeignKey( + name: "fk_account_roles_accounts_account_id", + column: x => x.account_id, + principalSchema: "infolink", + principalTable: "Accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_account_roles_roles_role_id", + column: x => x.role_id, + principalSchema: "infolink", + principalTable: "Roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + schema: "infolink", + table: "Roles", + columns: new[] { "id", "created_by", "created_on", "description", "is_system", "modified_by", "modified_on", "name", "permissions" }, + values: new object[,] + { + { 1, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Full access to everything, including members, roles and settings.", true, null, null, "Administrator", "[]" }, + { 2, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Runs and configures integrations. Can't manage members, roles or settings.", true, null, null, "Member", "[]" }, + { 3, null, new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), "Read-only access to integrations, exchanges and configuration.", true, null, null, "Viewer", "[]" } + }); + + migrationBuilder.CreateIndex( + name: "ix_account_roles_role_id", + schema: "infolink", + table: "AccountRoles", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "ix_roles_name", + schema: "infolink", + table: "Roles", + column: "name", + unique: true); + + // Every existing account keeps exactly the access it had: its coarse AccountRole + // (Admin=0, Viewer=10, Member=20) becomes the matching built-in role. Anything + // unrecognised lands on Viewer — least privilege. Idempotent, so re-running or + // ordering against the seeded admin account can't produce duplicates. + migrationBuilder.Sql(""" + INSERT INTO infolink."AccountRoles" (account_id, role_id) + SELECT a.id, CASE a."role" WHEN 0 THEN 1 WHEN 20 THEN 2 ELSE 3 END + FROM infolink."Accounts" a + WHERE NOT EXISTS ( + SELECT 1 FROM infolink."AccountRoles" l WHERE l.account_id = a.id); + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccountRoles", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "Roles", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260726151523_DropAccountPhone.Designer.cs b/SW.Bitween.PgSql/Migrations/20260726151523_DropAccountPhone.Designer.cs new file mode 100644 index 00000000..5c169d68 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260726151523_DropAccountPhone.Designer.cs @@ -0,0 +1,2284 @@ +// +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("20260726151523_DropAccountPhone")] + partial class DropAccountPhone + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + 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("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + 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, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", 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("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.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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + 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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260726151523_DropAccountPhone.cs b/SW.Bitween.PgSql/Migrations/20260726151523_DropAccountPhone.cs new file mode 100644 index 00000000..4955d9b5 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260726151523_DropAccountPhone.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class DropAccountPhone : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "phone", + schema: "infolink", + table: "Accounts"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "phone", + schema: "infolink", + table: "Accounts", + type: "character varying(20)", + unicode: false, + maxLength: 20, + nullable: true); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "Accounts", + keyColumn: "id", + keyValue: 9999, + column: "phone", + value: null); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260727133034_AddSettings.Designer.cs b/SW.Bitween.PgSql/Migrations/20260727133034_AddSettings.Designer.cs new file mode 100644 index 00000000..fcc5674b --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260727133034_AddSettings.Designer.cs @@ -0,0 +1,2318 @@ +// +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("20260727133034_AddSettings")] + partial class AddSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + 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("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + 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, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", 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("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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + 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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260727133034_AddSettings.cs b/SW.Bitween.PgSql/Migrations/20260727133034_AddSettings.cs new file mode 100644 index 00000000..332f5711 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260727133034_AddSettings.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Settings", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "character varying(200)", unicode: false, maxLength: 200, nullable: false), + value = table.Column(type: "text", nullable: true), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + created_by = table.Column(type: "text", nullable: true), + modified_on = table.Column(type: "timestamp with time zone", nullable: true), + modified_by = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_settings", x => x.id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Settings", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.Designer.cs b/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.Designer.cs new file mode 100644 index 00000000..7ad3ef75 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.Designer.cs @@ -0,0 +1,2175 @@ +// +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("20260811081200_SharedRetryGroupTotals")] + partial class SharedRetryGroupTotals + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "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.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") + .HasColumnType("integer") + .HasColumnName("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("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + 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("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("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.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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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("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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260811081200_SharedRetryGroupTotals.cs b/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.cs new file mode 100644 index 00000000..2bad0745 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class SharedRetryGroupTotals : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "group_attempt_counts", + schema: "infolink", + table: "xchange"); + + migrationBuilder.DropColumn( + name: "group_attempt_counts", + schema: "infolink", + table: "delayed_retry"); + + migrationBuilder.CreateTable( + name: "retry_group_usage", + schema: "infolink", + columns: table => new + { + subscription_id = table.Column(type: "integer", nullable: false), + group_id = table.Column(type: "uuid", nullable: false), + attempts_used = table.Column(type: "integer", nullable: false), + last_attempt_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_retry_group_usage", x => new { x.subscription_id, x.group_id }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "retry_group_usage", + schema: "infolink"); + + migrationBuilder.AddColumn>( + name: "group_attempt_counts", + schema: "infolink", + table: "xchange", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn>( + name: "group_attempt_counts", + schema: "infolink", + table: "delayed_retry", + type: "jsonb", + nullable: true); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs b/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs new file mode 100644 index 00000000..e7f65c75 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs @@ -0,0 +1,2180 @@ +// +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("20260811092318_RetryBlockedReason")] + partial class RetryBlockedReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "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.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") + .HasColumnType("integer") + .HasColumnName("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("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + 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("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("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.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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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("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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260811092318_RetryBlockedReason.cs b/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.cs new file mode 100644 index 00000000..e33c8ce0 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class RetryBlockedReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "retry_blocked_reason", + schema: "infolink", + table: "xchange_result", + type: "character varying(500)", + maxLength: 500, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "retry_blocked_reason", + schema: "infolink", + table: "xchange_result"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.Designer.cs b/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.Designer.cs new file mode 100644 index 00000000..cf14dea6 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.Designer.cs @@ -0,0 +1,2168 @@ +// +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("20260812092613_AddAccountLockout")] + partial class AddAccountLockout + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + 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") + .HasColumnType("integer") + .HasColumnName("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("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", 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("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.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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + 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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260812092613_AddAccountLockout.cs b/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.cs new file mode 100644 index 00000000..3577dd72 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddAccountLockout : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "failed_login_count", + schema: "infolink", + table: "Accounts", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "lockout_end", + schema: "infolink", + table: "Accounts", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "Accounts", + keyColumn: "id", + keyValue: 9999, + columns: new[] { "failed_login_count", "lockout_end" }, + values: new object[] { 0, null }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "failed_login_count", + schema: "infolink", + table: "Accounts"); + + migrationBuilder.DropColumn( + name: "lockout_end", + schema: "infolink", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.Designer.cs b/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.Designer.cs new file mode 100644 index 00000000..f8b2d158 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.Designer.cs @@ -0,0 +1,2242 @@ +// +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("20260817103433_RetryBudgetAlerts")] + partial class RetryBudgetAlerts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "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.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") + .HasColumnType("integer") + .HasColumnName("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("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + 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("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("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.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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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("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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260817103433_RetryBudgetAlerts.cs b/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.cs new file mode 100644 index 00000000..ed6b8724 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.cs @@ -0,0 +1,137 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class RetryBudgetAlerts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "attempt_number", + schema: "infolink", + table: "xchange_result", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "retry_group_id", + schema: "infolink", + table: "xchange_result", + type: "uuid", + nullable: true); + + migrationBuilder.AlterColumn( + name: "notifier_id", + schema: "infolink", + table: "xchange_notification", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AddColumn( + name: "alert_handler_id", + schema: "infolink", + table: "retry_policy", + type: "character varying(200)", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "alert_handler_properties", + schema: "infolink", + table: "retry_policy", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "exhausted_notified_on", + schema: "infolink", + table: "retry_group_usage", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.CreateTable( + name: "retry_alert_override", + schema: "infolink", + columns: table => new + { + subscription_id = table.Column(type: "integer", nullable: false), + group_id = table.Column(type: "uuid", nullable: false), + alert_mode = table.Column(type: "smallint", nullable: false), + alert_handler_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + alert_handler_properties = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_retry_alert_override", x => new { x.subscription_id, x.group_id }); + }); + + migrationBuilder.CreateIndex( + name: "ix_xchange_result_retry_group_id", + schema: "infolink", + table: "xchange_result", + column: "retry_group_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "retry_alert_override", + schema: "infolink"); + + migrationBuilder.DropIndex( + name: "ix_xchange_result_retry_group_id", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "attempt_number", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "retry_group_id", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "alert_handler_id", + schema: "infolink", + table: "retry_policy"); + + migrationBuilder.DropColumn( + name: "alert_handler_properties", + schema: "infolink", + table: "retry_policy"); + + migrationBuilder.DropColumn( + name: "exhausted_notified_on", + schema: "infolink", + table: "retry_group_usage"); + + // These rows are the alert's own delivery log, and they are the reason the column was + // made nullable. Rolling the feature back leaves nowhere to put them, and the column + // cannot go back to NOT NULL while they are here, so they go with the feature. + migrationBuilder.Sql( + "DELETE FROM infolink.xchange_notification WHERE notifier_id IS NULL;"); + + migrationBuilder.AlterColumn( + name: "notifier_id", + schema: "infolink", + table: "xchange_notification", + type: "integer", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.Designer.cs b/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.Designer.cs new file mode 100644 index 00000000..12ed1d41 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.Designer.cs @@ -0,0 +1,2246 @@ +// +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("20260819100439_ManualRetryFlag")] + partial class ManualRetryFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "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.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") + .HasColumnType("integer") + .HasColumnName("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("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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("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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + 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("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("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.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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260819100439_ManualRetryFlag.cs b/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.cs new file mode 100644 index 00000000..31efce51 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class ManualRetryFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "manual_retry", + schema: "infolink", + table: "xchange", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "manual_retry", + schema: "infolink", + table: "xchange"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.Designer.cs b/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.Designer.cs new file mode 100644 index 00000000..dccb27e0 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.Designer.cs @@ -0,0 +1,2413 @@ +// +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("20260823104201_GatewayInactiveFlag")] + partial class GatewayInactiveFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + 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.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("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + 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, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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.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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + 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("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("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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260823104201_GatewayInactiveFlag.cs b/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.cs new file mode 100644 index 00000000..459832fe --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class GatewayInactiveFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "inactive", + schema: "infolink", + table: "bus_gateway", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "inactive", + schema: "infolink", + table: "api_gateway", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "inactive", + schema: "infolink", + table: "bus_gateway"); + + migrationBuilder.DropColumn( + name: "inactive", + schema: "infolink", + table: "api_gateway"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.Designer.cs b/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.Designer.cs new file mode 100644 index 00000000..c1722c67 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.Designer.cs @@ -0,0 +1,2455 @@ +// +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("20260824093537_AddReceiveAttempts")] + partial class AddReceiveAttempts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + 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.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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + 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.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("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + 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, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", 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("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.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("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + 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("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.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.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.Property("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("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + 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.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("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + 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.Property("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("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("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("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>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + 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("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + 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("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("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("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("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.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "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("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.Property("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("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.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.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("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.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + 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("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + 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("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "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.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.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + 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.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + 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("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.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", null) + .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("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + 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.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_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.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/20260824093537_AddReceiveAttempts.cs b/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.cs new file mode 100644 index 00000000..cee14373 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddReceiveAttempts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "receive_attempt", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + subscription_id = table.Column(type: "integer", nullable: false), + started_on = table.Column(type: "timestamp with time zone", nullable: false), + finished_on = table.Column(type: "timestamp with time zone", nullable: false), + outcome = table.Column(type: "integer", nullable: false), + error_message = table.Column(type: "text", nullable: true), + exchange_ids = table.Column(type: "text[]", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_receive_attempt", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_receive_attempt_subscription_id_started_on", + schema: "infolink", + table: "receive_attempt", + columns: new[] { "subscription_id", "started_on" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "receive_attempt", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs index 8cea92fe..1a37f6f0 100644 --- a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -66,6 +66,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("smallint") .HasColumnName("email_provider"); + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + b.Property("LoginMethods") .HasColumnType("smallint") .HasColumnName("login_methods"); @@ -84,12 +92,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(500)") .HasColumnName("password"); - b.Property("Phone") - .HasMaxLength(20) - .IsUnicode(false) - .HasColumnType("character varying(20)") - .HasColumnName("phone"); - b.Property("Role") .HasColumnType("integer") .HasColumnName("role"); @@ -113,12 +115,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) DisplayName = "Admin", Email = "admin@Bitween.systems", EmailProvider = (byte)0, + FailedLoginCount = 0, LoginMethods = (byte)2, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", Role = 0 }); }); + 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") @@ -148,6 +170,89 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 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(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => { b.Property("Id") @@ -155,10 +260,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)") .HasColumnName("id"); - b.Property>("GroupAttemptCounts") - .HasColumnType("jsonb") - .HasColumnName("group_attempt_counts"); - b.Property("On") .HasColumnType("timestamp with time zone") .HasColumnName("on"); @@ -175,9 +276,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); @@ -187,6 +291,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -216,6 +325,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnique() .HasDatabaseName("ix_document_bus_message_type_name"); + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + b.HasIndex("Name") .IsUnique() .HasDatabaseName("ix_document_name"); @@ -293,6 +406,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("created_on"); + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + b.Property("ModifiedBy") .HasColumnType("text") .HasColumnName("modified_by"); @@ -386,6 +503,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("document_id"); + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + b.Property("ModifiedBy") .HasColumnType("text") .HasColumnName("modified_by"); @@ -606,6 +727,105 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + 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.Property("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("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") @@ -615,6 +835,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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"); @@ -647,6 +876,40 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -941,10 +1204,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("document_id"); - b.Property>("GroupAttemptCounts") - .HasColumnType("jsonb") - .HasColumnName("group_attempt_counts"); - b.Property("HandlerId") .HasMaxLength(200) .HasColumnType("character varying(200)") @@ -974,6 +1233,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("input_size"); + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + b.Property("MapperId") .HasMaxLength(200) .HasColumnType("character varying(200)") @@ -1097,7 +1360,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("finished_on"); - b.Property("NotifierId") + b.Property("NotifierId") .HasColumnType("integer") .HasColumnName("notifier_id"); @@ -1156,6 +1419,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)") .HasColumnName("id"); + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + b.Property("Exception") .HasColumnType("text") .HasColumnName("exception"); @@ -1214,6 +1481,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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("Success") .HasColumnType("boolean") .HasColumnName("success"); @@ -1221,6 +1497,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_xchange_result"); + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + b.ToTable("xchange_result", "infolink"); }); @@ -1767,6 +2046,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("qrtz_triggers", "infolink"); }); + 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) diff --git a/SW.Bitween.PgSql/SW.Bitween.PgSql.csproj b/SW.Bitween.PgSql/SW.Bitween.PgSql.csproj index 4bfbb7ba..1900129c 100644 --- a/SW.Bitween.PgSql/SW.Bitween.PgSql.csproj +++ b/SW.Bitween.PgSql/SW.Bitween.PgSql.csproj @@ -1,14 +1,14 @@ - net8.0 + net10.0 SW.Bitween.PgSql - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj b/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj index e88ef705..574f6962 100644 --- a/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj +++ b/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net10.0 SW.Bitween.SampleConfigurableAdapter diff --git a/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj b/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj index e1f52c9c..0a1f988f 100644 --- a/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj +++ b/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 SW.Bitween.SampleHandler diff --git a/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj b/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj index 407d145d..b98f76c9 100644 --- a/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj +++ b/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 SW.Bitween.SampleMapper diff --git a/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj b/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj index 8daa047e..b9dc71af 100644 --- a/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj +++ b/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 SW.Bitween.SampleValidator diff --git a/SW.Bitween.Sdk/Model/Account.cs b/SW.Bitween.Sdk/Model/Account.cs index c112c9d4..b1921407 100644 --- a/SW.Bitween.Sdk/Model/Account.cs +++ b/SW.Bitween.Sdk/Model/Account.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace SW.Bitween.Model; @@ -7,13 +8,26 @@ public class CreateAccountModel public string Name { get; set; } public string Email { get; set; } public string Password { get; set; } - public int Role { get; set; } + + /// + /// Legacy coarse role. Nullable on purpose: when it was a plain int, a request that omitted it + /// sent 0 — which is Admin — so a member added with no roles came out an administrator. + /// + public int? Role { get; set; } + + /// Roles to grant. Preferred over , which is legacy. + public List RoleIds { get; set; } = []; } public class UpdateAccountModel { public string Name { get; set; } - public int Role { get; set; } + + /// + /// Legacy coarse role. Nullable on purpose: when it was a plain int, a request that omitted it + /// sent 0 — which is Admin. Only supplied values are applied, and only with users.edit. + /// + public int? Role { get; set; } } public class RemoveAccountModel @@ -27,14 +41,39 @@ public class SearchMembersModel public bool Lookup { get; set; } } +public class AccountRoleSummary +{ + public int Id { get; set; } + public string Name { get; set; } +} + public class AccountModel { public string Name { get; set; } public int Id { get; set; } public string Email { get; set; } + /// + /// Legacy coarse role, kept for older clients. Authorization reads . + /// public string Role { get; set; } + + public bool Disabled { get; set; } public DateTime CreatedOn { get; set; } + public List Roles { get; set; } = []; + + // Non-null and in the future => the account is currently locked out. + public DateTime? LockoutEnd { get; set; } +} + +/// The signed-in account, plus everything the UI needs to decide what to show. +public class ProfileModel : AccountModel +{ + public List Permissions { get; set; } = []; +} + +public class UnlockAccountModel +{ } public class ChangePasswordModel @@ -42,4 +81,21 @@ public class ChangePasswordModel public string NewPassword { get; set; } public string OldPassword { get; set; } -} \ No newline at end of file +} + +/// Replaces the whole set of roles a member holds. +public class SetAccountRolesModel +{ + public List RoleIds { get; set; } = []; +} + +public class SetAccountDisabledModel +{ + public bool Disabled { get; set; } +} + +/// An administrator setting someone else's password, standing in for a reset flow. +public class SetAccountPasswordModel +{ + public string Password { get; set; } +} diff --git a/SW.Bitween.Sdk/Model/ApiGateway.cs b/SW.Bitween.Sdk/Model/ApiGateway.cs index dfb8a562..2bd43160 100644 --- a/SW.Bitween.Sdk/Model/ApiGateway.cs +++ b/SW.Bitween.Sdk/Model/ApiGateway.cs @@ -7,6 +7,9 @@ public class ApiGatewayCreate : IName { public string Name { get; set; } public string UrlName { get; set; } + + /// Off but kept, with its partner attachments. Calls to it are refused. + public bool Inactive { get; set; } } public class ApiGatewayRow : ApiGatewayUpdate @@ -31,7 +34,22 @@ public class ApiGatewayPartnerDto public class ApiGatewayPartnerCreate { public int PartnerId { get; set; } - public int SubscriptionId { get; set; } + + /// An integration that already exists. Exactly one of this and + /// is given. + public int? SubscriptionId { get; set; } + + /// Define the integration here instead of creating it first. It is created as a + /// GatewayApiCall in the same transaction as the attachment. + public InlineIntegrationCreate NewIntegration { get; set; } + } + + public class SearchApiGatewayAttachmentsModel + { + public int ApiGatewayId { get; set; } + public string Search { get; set; } + public int? Offset { get; set; } + public int? Limit { get; set; } } } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs b/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs new file mode 100644 index 00000000..06a9a22a --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SW.Bitween.Model; + +/// +/// The outcome of asking a 's shared total budget for one attempt. +/// +/// +/// true when a slot was claimed and a retry may be scheduled. +/// +/// +/// true only for the single caller that first found the budget spent, so an +/// exhaustion alert is raised once rather than on every failure that follows. +/// Always false when is true. +/// +public readonly record struct RetryBudgetClaim(bool Granted, bool JustExhausted) +{ + /// A slot was claimed. + public static RetryBudgetClaim Allowed => new(true, false); + + /// No slot available, and someone else has already taken responsibility for alerting. + public static RetryBudgetClaim Denied => new(false, false); + + /// No slot available, and this caller owns the alert for it. + public static RetryBudgetClaim DeniedAndJustExhausted => new(false, true); +} + +/// +/// Tracks how much of a 's +/// has already been spent. +/// +/// +/// The total is a ceiling shared by every message that hits the group, so the count cannot +/// live on the message being evaluated — it needs a store that outlives a single xchange. +/// The implementation decides the scope: the production one keys by integration + group and +/// persists, while counts only for one dry-run. +/// +public interface IRetryGroupBudget +{ + /// Claims one attempt from the group's total budget. + Task TryConsume(Guid groupId, int maxAttemptsTotal); +} + +/// +/// In-memory for the policy dry-run endpoint, where nothing +/// should be persisted and the budget spans only the simulated run. +/// +public class InMemoryRetryGroupBudget : IRetryGroupBudget +{ + private readonly Dictionary _used = new(); + + /// + /// + /// Never reports : simulating a policy must not + /// send anyone an alert. + /// + public Task TryConsume(Guid groupId, int maxAttemptsTotal) + { + var used = _used.GetValueOrDefault(groupId, 0); + if (used >= maxAttemptsTotal) return Task.FromResult(RetryBudgetClaim.Denied); + + _used[groupId] = used + 1; + return Task.FromResult(RetryBudgetClaim.Allowed); + } +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs b/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs index 4b6f6146..92eaa763 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs @@ -34,7 +34,7 @@ public enum JsonPathOp /// /// /// For groups the content is the exception stack-trace text. -/// For groups the content is the raw JSON response string. +/// For groups the content is the raw response body. /// Matcher implementations are serialised polymorphically via System.Text.Json. /// [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] @@ -44,8 +44,13 @@ public enum JsonPathOp [JsonDerivedType(typeof(JsonPathMatcher), typeDiscriminator: "jsonPath")] public abstract class Matcher { - /// The result type this matcher operates on. - public abstract XchangeResultType ResultType { get; } + /// + /// Returns true when this matcher can be evaluated against + /// content. The evaluator skips incompatible matchers, + /// so a group whose matchers all return false here can never fire for that + /// result type. + /// + public abstract bool Supports(XchangeResultType resultType); /// /// Returns true when satisfies this matcher's condition. @@ -54,16 +59,17 @@ public abstract class Matcher public abstract bool IsMatch(string content); } -// ── Error matchers ──────────────────────────────────────────────────────────── +// ── Text matchers (Error and BadResult) ─────────────────────────────────────── /// -/// Matches when the exception text contains a literal substring. -/// Applies to content. +/// Matches when the failure text contains a literal substring — the exception text for +/// , the response body for . /// public class ContainsMatcher : Matcher { /// - public override XchangeResultType ResultType => XchangeResultType.Error; + public override bool Supports(XchangeResultType resultType) => + resultType is XchangeResultType.Error or XchangeResultType.BadResult; /// The substring to search for. public required string Value { get; init; } @@ -78,13 +84,14 @@ public override bool IsMatch(string content) => } /// -/// Matches when the exception text satisfies a regular expression. -/// Applies to content. +/// Matches when the failure text satisfies a regular expression — the exception text for +/// , the response body for . /// public class RegexMatcher : Matcher { /// - public override XchangeResultType ResultType => XchangeResultType.Error; + public override bool Supports(XchangeResultType resultType) => + resultType is XchangeResultType.Error or XchangeResultType.BadResult; /// .NET-compatible regular expression pattern. public required string Pattern { get; init; } @@ -106,6 +113,8 @@ public class RegexMatcher : Matcher public override bool IsMatch(string content) => Compiled.IsMatch(content); } +// ── Error-only matcher ──────────────────────────────────────────────────────── + /// /// Matches when the exception text mentions a specific .NET exception type name, /// scanning the entire stack-trace including inner exceptions. @@ -118,7 +127,8 @@ public class RegexMatcher : Matcher public class ExceptionTypeMatcher : Matcher { /// - public override XchangeResultType ResultType => XchangeResultType.Error; + public override bool Supports(XchangeResultType resultType) => + resultType == XchangeResultType.Error; /// /// Fully-qualified or short exception type name, e.g. "System.TimeoutException" @@ -163,7 +173,8 @@ public override bool IsMatch(string content) public class JsonPathMatcher : Matcher { /// - public override XchangeResultType ResultType => XchangeResultType.BadResult; + public override bool Supports(XchangeResultType resultType) => + resultType == XchangeResultType.BadResult; /// JSONPath expression, e.g. "$.error.code" or "$.lines[0].status". public required string Path { get; init; } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs new file mode 100644 index 00000000..bc4d2113 --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs @@ -0,0 +1,39 @@ +namespace SW.Bitween.Model; + +/// +/// Whether a level of the alert hierarchy defines its own destination for +/// "retry budget exhausted" alerts, or defers to the level above it. +/// +/// +/// The hierarchy is resolved per failing subscription and group, most specific first: +/// the subscription+group override, then the group, then the policy. An overriding level +/// replaces the level above rather than merging into it, so the handler and +/// every property it needs must be present on whichever level wins. +/// +public enum RetryAlertMode +{ + /// Defer to the level above. The default, so existing policies keep behaving as before. + Inherit, + + /// Send through this level's own handler, ignoring anything configured above it. + Send, + + /// Send nothing, and stop the walk — an alert configured above is deliberately suppressed here. + Silent, +} + +/// +/// Which level of the hierarchy decided where an alert goes. Surfaced in the management UI so a +/// destination that looks wrong can be traced to the level that set it. +/// +public enum RetryAlertLevel +{ + /// An override for this one subscription and group. + SubscriptionGroup, + + /// The group's own setting, applying to every subscription using the policy. + Group, + + /// The policy default. + Policy, +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs index 65c057bc..6811e3c4 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs @@ -59,6 +59,22 @@ public class RetryGroup /// Optional free-text notes visible in the management UI. public string? Notes { get; init; } + + /// + /// Whether this group defines its own destination for budget-exhausted alerts, suppresses the + /// policy's, or defers to it. Defaults to so groups saved + /// before alerts existed keep using the policy's setting. + /// + public RetryAlertMode AlertMode { get; init; } = RetryAlertMode.Inherit; + + /// + /// Adapter that delivers this group's alert. Required when is + /// , ignored otherwise. + /// + public string? AlertHandlerId { get; init; } + + /// That adapter's own settings — api key, recipients, subject. + public Dictionary? AlertHandlerProperties { get; init; } } /// @@ -73,8 +89,11 @@ public class RetryBudget public int MaxAttemptsPerError { get; init; } /// - /// Hard ceiling on the total number of group-level retries across all messages in the - /// current processing window. Prevents a burst of failures from hammering the downstream. + /// Hard ceiling on the total number of group-level retries across all messages, counted per + /// subscription so one shared policy does not let a single noisy subscription spend everyone's + /// allowance. Prevents a burst of failures from hammering the downstream. It is not a rate over a + /// rolling window: the count only falls once it has been reached — the subscription's next success + /// then lifts it — or when somebody resets it by hand. /// public int MaxAttemptsTotal { get; init; } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs index edc9fa86..97ba93d4 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace SW.Bitween.Model; @@ -10,47 +10,18 @@ namespace SW.Bitween.Model; /// /// /// -/// Stateful per processing window. The evaluator accumulates -/// per-group attempt totals in _groupAttemptCounts. For a single in-process -/// window (e.g. a running RetryJob batch) one instance handles all messages. +/// Two independent caps. is +/// per message and is derived from the caller's attemptIndexForThisMessage, while +/// is shared by every message hitting the group and +/// is owned by the injected . The evaluator itself keeps no +/// counters, so a fresh instance per failure enforces both caps correctly. /// /// -/// Cross-invocation persistence. When a retry is scheduled the -/// current counts are saved to and stored on -/// the DelayedRetry entity (and on the new Xchange so that a later -/// failure of the retry itself can pick up where the budgets left off). On the next -/// evaluation call before . -/// -/// -/// Not thread-safe. Each goroutine/task should use its own instance. +/// Not thread-safe. Each task should use its own instance. /// /// -public class RetryPolicyEvaluator(IRetryPolicy policy) +public class RetryPolicyEvaluator(IRetryPolicy policy, IRetryGroupBudget groupBudget) { - private readonly Dictionary _groupAttemptCounts = new(); - - /// - /// Restores previously persisted group-level attempt counts, allowing budgets - /// to continue from where they left off across separate process invocations. - /// - /// - /// The dictionary returned by from a prior evaluation. - /// String keys are parsed back to — invalid entries are silently ignored. - /// - public void RestoreGroupAttemptCounts(Dictionary counts) - { - foreach (var kv in counts) - if (Guid.TryParse(kv.Key, out var guid)) - _groupAttemptCounts[guid] = kv.Value; - } - - /// - /// Returns the current group-level attempt counts as a string-keyed dictionary - /// suitable for JSON serialisation and storage on DelayedRetry / Xchange. - /// - public Dictionary GetGroupAttemptCounts() => - _groupAttemptCounts.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value); - /// /// Evaluates the policy and returns a retry decision for the failed xchange. /// @@ -69,7 +40,7 @@ public Dictionary GetGroupAttemptCounts() => /// /// A indicating whether to retry and, if so, how long to wait. /// - public RetryDecision Evaluate( + public async Task Evaluate( XchangeResultType resultType, string content, int attemptIndexForThisMessage) @@ -83,23 +54,32 @@ public RetryDecision Evaluate( return RetryDecision.Block("No matching group (default block)"); if (group.Action == RetryAction.Block) - return RetryDecision.Block($"Group '{group.Name}' explicitly blocks this error"); + return RetryDecision.Block($"Group '{group.Name}' explicitly blocks this error", group); - var budget = group.Budget!; + // A group that allows retries without saying how many is refused rather than trusted: the + // dereference used to throw here, and because the caller logs and swallows that, retries just + // stopped happening with no reason recorded anywhere. Validation keeps new policies out of this + // state; this is for the ones already saved in it. + if (group.Budget is null) + return RetryDecision.Block( + $"Group '{group.Name}' allows retries but has no budget, so none can be scheduled", group); + + var budget = group.Budget; if (attemptIndexForThisMessage >= budget.MaxAttemptsPerError) return RetryDecision.Block( - $"Per-message cap reached ({budget.MaxAttemptsPerError}) in group '{group.Name}'"); + $"Per-message cap reached ({budget.MaxAttemptsPerError}) in group '{group.Name}'", group); - var totalUsed = _groupAttemptCounts.GetValueOrDefault(group.Id, 0); - if (totalUsed >= budget.MaxAttemptsTotal) + // Claimed last so a message already stopped by its own per-message cap doesn't + // eat a slot out of the shared total. + var claim = await groupBudget.TryConsume(group.Id, budget.MaxAttemptsTotal); + if (!claim.Granted) return RetryDecision.Block( - $"Group total cap reached ({budget.MaxAttemptsTotal}) for group '{group.Name}'"); - - _groupAttemptCounts[group.Id] = totalUsed + 1; + $"Group total cap reached ({budget.MaxAttemptsTotal}) for group '{group.Name}'", group, + claim.JustExhausted); var delay = budget.DelayStrategy.GetDelay(attemptIndexForThisMessage); - return RetryDecision.Allow(delay, group.Name); + return RetryDecision.Allow(delay, group); } private RetryGroup? FindMatchingGroup(XchangeResultType resultType, string content) @@ -108,7 +88,11 @@ public RetryDecision Evaluate( .Where(g => g.Enabled && g.AppliesTo.Contains(resultType)) .OrderBy(g => g.Priority)) { - var compatibleMatchers = group.Matchers.Where(m => m.ResultType == resultType); + // No conditions configured at all means "match every applicable failure". + if (group.Matchers.Count == 0) + return group; + + var compatibleMatchers = group.Matchers.Where(m => m.Supports(resultType)); if (compatibleMatchers.Any(m => m.IsMatch(content))) return group; } @@ -130,22 +114,44 @@ public class RetryDecision /// Human-readable explanation of the decision, useful for audit/debug logs. public string Reason { get; private init; } = ""; - /// Name of the that matched, or null when blocked. - public string? MatchedGroupName { get; private init; } + /// + /// The group that matched this failure, or null when none did. Set on blocked + /// decisions too — internal callers (attempt tracking, exhaustion alerts) need to know which + /// group refused a failure, not only which group allowed one. + /// + public RetryGroup? MatchedGroup { get; private init; } + + /// + /// Name of the that allowed a retry, or null when blocked — + /// including when a group matched but refused. A refusal by a matched group and a failure no + /// group was ever configured to catch should look the same to a caller that only cares whether + /// something is retrying, which is what is for instead. + /// + public string? MatchedGroupName => ShouldRetry ? MatchedGroup?.Name : null; + + /// + /// true when this decision was the one that found the group's shared total spent for + /// the first time, meaning the caller owes an exhaustion alert. Never true twice for + /// the same subscription and group until the budget is reset. + /// + public bool BudgetJustExhausted { get; private init; } /// Creates an Allow decision — a retry will be scheduled after . - public static RetryDecision Allow(TimeSpan delay, string groupName) => new() + public static RetryDecision Allow(TimeSpan delay, RetryGroup group) => new() { ShouldRetry = true, Delay = delay, - MatchedGroupName = groupName, - Reason = $"Allowed by group '{groupName}'" + MatchedGroup = group, + Reason = $"Allowed by group '{group.Name}'" }; /// Creates a Block decision — no retry will be scheduled. - public static RetryDecision Block(string reason) => new() + public static RetryDecision Block(string reason, RetryGroup? group = null, + bool budgetJustExhausted = false) => new() { ShouldRetry = false, - Reason = reason + Reason = reason, + MatchedGroup = group, + BudgetJustExhausted = budgetJustExhausted }; } diff --git a/SW.Bitween.Sdk/Model/BusGateway.cs b/SW.Bitween.Sdk/Model/BusGateway.cs index 9240b24a..9e103156 100644 --- a/SW.Bitween.Sdk/Model/BusGateway.cs +++ b/SW.Bitween.Sdk/Model/BusGateway.cs @@ -7,6 +7,9 @@ public class BusGatewayCreate : IName { public string Name { get; set; } public int DocumentId { get; set; } + + /// Off but kept, with its routes. Messages stop reaching them. + public bool Inactive { get; set; } } public class BusGatewayUpdate : BusGatewayCreate @@ -33,7 +36,14 @@ public class BusGatewayRouteDto public class BusGatewayRouteCreate { - public int SubscriptionId { get; set; } + /// An integration that already exists. Exactly one of this and + /// is given. + public int? SubscriptionId { get; set; } + + /// Define the integration here instead of creating it first. It is created + /// carrying the gateway's own information type, in the same transaction as the route. + public InlineIntegrationCreate NewIntegration { get; set; } + public int? PartnerId { get; set; } public IPropertyMatchSpecification MatchExpression { get; set; } } @@ -43,6 +53,13 @@ public class BusGatewayRouteUpdate : BusGatewayRouteCreate public int RouteId { get; set; } } + /// Shared by the two gateway kinds: which integration a link points at. + public static class GatewayLinkTarget + { + public const string BothGiven = "INTEGRATION_AMBIGUOUS"; + public const string NeitherGiven = "INTEGRATION_REQUIRED"; + } + public class RemoveRouteRequest { public int RouteId { get; set; } diff --git a/SW.Bitween.Sdk/Model/DelayedRetryModel.cs b/SW.Bitween.Sdk/Model/DelayedRetryModel.cs index cc712856..06eb40fd 100644 --- a/SW.Bitween.Sdk/Model/DelayedRetryModel.cs +++ b/SW.Bitween.Sdk/Model/DelayedRetryModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace SW.Bitween.Model; @@ -12,6 +13,23 @@ public class DelayedRetryRow public string DocumentName { get; set; } public string Exception { get; set; } public DateTime StartedOn { get; set; } + + /// + /// The exchange's promoted properties, so a pending retry can identify itself + /// by what it carries (order number, store…) instead of only by its id. + /// Null when the document type promotes nothing. + /// + public IDictionary PromotedProperties { get; set; } + + /// + /// The shared retry policy the subscription currently points at. Null when the + /// subscription carries an inline CustomRetryPolicy instead — a delayed retry + /// can only be scheduled by one or the other, so null means "look on the subscription". + /// Reflects the policy as it stands now, not necessarily the one that set . + /// + public int? RetryPolicyId { get; set; } + + public string RetryPolicyName { get; set; } } public class DelayedRetryRunNow diff --git a/SW.Bitween.Sdk/Model/Document.cs b/SW.Bitween.Sdk/Model/Document.cs index ed001b91..40b0eba9 100644 --- a/SW.Bitween.Sdk/Model/Document.cs +++ b/SW.Bitween.Sdk/Model/Document.cs @@ -14,9 +14,18 @@ public enum DocumentFormat public class DocumentCreate : IName { - public int Id { get; set; } + public string Code { get; set; } public DocumentFormat DocumentFormat { get; set; } public string Name { get; set; } + public bool BusEnabled { get; set; } + public string BusMessageTypeName { get; set; } + public int DuplicateInterval { get; set; } + + public bool DisregardsUnfilteredMessages { get; set; } + + /// Carried on create too, so a new type arrives complete rather than + /// needing a second save before it can be filtered on. + public ICollection PromotedProperties { get; set; } } public class SearchDocumentTrailModel @@ -33,13 +42,7 @@ public class DocumentTrailModel : TrailBaseModel public class DocumentUpdate : DocumentCreate { - public bool BusEnabled { get; set; } - public string BusMessageTypeName { get; set; } - public int DuplicateInterval { get; set; } - - public bool DisregardsUnfilteredMessages { get; set; } - - public ICollection PromotedProperties { get; set; } + public int Id { get; set; } } public class DocumentRow : DocumentUpdate diff --git a/SW.Bitween.Sdk/Model/Notifier.cs b/SW.Bitween.Sdk/Model/Notifier.cs index dc388f59..c238a207 100644 --- a/SW.Bitween.Sdk/Model/Notifier.cs +++ b/SW.Bitween.Sdk/Model/Notifier.cs @@ -34,6 +34,8 @@ public class NotifierSearch public bool? RunOnFailedResult { get; set; } public string HandlerId { get; set; } public bool? Inactive { get; set; } + /// So the list page can show a watched-integration count without a per-row detail fetch. + public int[] RunOnSubscriptions { get; set; } } diff --git a/SW.Bitween.Sdk/Model/Partner.cs b/SW.Bitween.Sdk/Model/Partner.cs index fa977724..20413464 100644 --- a/SW.Bitween.Sdk/Model/Partner.cs +++ b/SW.Bitween.Sdk/Model/Partner.cs @@ -7,19 +7,31 @@ namespace SW.Bitween.Model public class PartnerCreate : IName { public string Name { get; set; } + + /// + /// Referenced from adapter fields as {{partner.KEY}}. Accepted at creation so a + /// partner can be made complete in one call — the UI creates partners from + /// inside other flows, where a follow-up update that fails would leave a + /// partner whose adapters resolve nothing. + /// + public Dictionary AdapterProperties { get; set; } } public class PartnerRow : PartnerUpdate { public int Id { get; set; } public int? SubscriptionsCount { get; set; } public int? Keys { get; set; } - + /// + /// The names of the partner's adapter properties — never their values, which + /// can be secrets. Names alone are enough to count them in a list and to offer + /// them as {{partner.x}} reference tokens when configuring an adapter. + /// + public ICollection PropertyKeys { get; set; } } public class PartnerUpdate : PartnerCreate { public ICollection ApiCredentials { get; set; } public ICollection Subscriptions { get; set; } - public Dictionary AdapterProperties { get; set; } } } diff --git a/SW.Bitween.Sdk/Model/Permissions.cs b/SW.Bitween.Sdk/Model/Permissions.cs new file mode 100644 index 00000000..083d1bc2 --- /dev/null +++ b/SW.Bitween.Sdk/Model/Permissions.cs @@ -0,0 +1,270 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SW.Bitween.Model; + +/// +/// Every privilege Bitween recognises, as "<area>.<action>" keys. This is the single +/// source of truth: handlers guard on these constants, roles store the keys, and the +/// management UI renders the catalog served by GET /permissions. A unit test asserts +/// these constants and never drift apart. +/// +public static class Permissions +{ + public static class Exchanges + { + public const string View = "exchanges.view"; + public const string Operate = "exchanges.operate"; + } + + public static class Monitoring + { + public const string View = "monitoring.view"; + } + + public static class Dashboard + { + public const string View = "dashboard.view"; + } + + public static class Subscriptions + { + public const string View = "subscriptions.view"; + public const string Create = "subscriptions.create"; + public const string Edit = "subscriptions.edit"; + public const string Delete = "subscriptions.delete"; + public const string Operate = "subscriptions.operate"; + } + + public static class Partners + { + public const string View = "partners.view"; + public const string Create = "partners.create"; + public const string Edit = "partners.edit"; + public const string Delete = "partners.delete"; + } + + public static class Documents + { + public const string View = "documents.view"; + public const string Create = "documents.create"; + public const string Edit = "documents.edit"; + public const string Delete = "documents.delete"; + } + + public static class GlobalValues + { + public const string View = "global-values.view"; + public const string Create = "global-values.create"; + public const string Edit = "global-values.edit"; + public const string Delete = "global-values.delete"; + } + + public static class Notifiers + { + public const string View = "notifiers.view"; + public const string Create = "notifiers.create"; + public const string Edit = "notifiers.edit"; + public const string Delete = "notifiers.delete"; + } + + public static class ApiGateways + { + public const string View = "api-gateways.view"; + public const string Create = "api-gateways.create"; + public const string Edit = "api-gateways.edit"; + public const string Delete = "api-gateways.delete"; + } + + public static class BusGateways + { + public const string View = "bus-gateways.view"; + public const string Create = "bus-gateways.create"; + public const string Edit = "bus-gateways.edit"; + public const string Delete = "bus-gateways.delete"; + } + + public static class WorkGroups + { + public const string View = "workgroups.view"; + public const string Create = "workgroups.create"; + public const string Edit = "workgroups.edit"; + public const string Delete = "workgroups.delete"; + } + + /// + /// No operate: running a scheduled retry now is gated on , + /// because what's being retried is an exchange, not the policy that scheduled it. + /// + public static class RetryPolicies + { + public const string View = "retry-policies.view"; + public const string Create = "retry-policies.create"; + public const string Edit = "retry-policies.edit"; + public const string Delete = "retry-policies.delete"; + } + + public static class Users + { + public const string View = "users.view"; + public const string Create = "users.create"; + public const string Edit = "users.edit"; + public const string Delete = "users.delete"; + } + + public static class Roles + { + public const string View = "roles.view"; + public const string Create = "roles.create"; + public const string Edit = "roles.edit"; + public const string Delete = "roles.delete"; + } + + public static class Settings + { + public const string View = "settings.view"; + public const string Edit = "settings.edit"; + } +} + +public class PermissionActionModel +{ + public string Id { get; set; } + + /// What this specific grant allows, in end-user words. + public string Description { get; set; } +} + +public class PermissionAreaModel +{ + public string Id { get; set; } + public string Label { get; set; } + + /// Mirrors the app's navigation groups, so a role's grants map onto what its members see. + public string Group { get; set; } + + public string Description { get; set; } + public List Actions { get; set; } = []; +} + +public static class PermissionCatalog +{ + public const string View = "view"; + public const string Create = "create"; + public const string Edit = "edit"; + public const string Delete = "delete"; + public const string Operate = "operate"; + + private static PermissionAreaModel Area(string id, string label, string group, string description, + params (string Id, string Description)[] actions) => new() + { + Id = id, + Label = label, + Group = group, + Description = description, + Actions = actions.Select(a => new PermissionActionModel { Id = a.Id, Description = a.Description }).ToList() + }; + + public static readonly List Areas = + [ + // ——— Operate ——— + Area("exchanges", "Exchanges", "Operate", "Every message that flows through Bitween.", + (View, "Browse exchanges, payloads and traces."), + (Operate, "Retry or resubmit failed exchanges.")), + + Area("monitoring", "Queue health", "Operate", "Live message-queue throughput and consumers.", + (View, "See queue health and rates.")), + + Area("dashboard", "Dashboard", "Operate", "Traffic and health overview (reached from the logo).", + (View, "See the dashboard.")), + + // ——— Integrations ——— + Area("subscriptions", "Integrations", "Integrations", "The configured pipelines that process exchanges.", + (View, "Browse integrations and their configuration."), + (Create, "Create integrations."), + (Edit, "Change adapters, mappings and settings."), + (Delete, "Delete integrations."), + (Operate, "Pause, resume, receive now, aggregate now.")), + + Area("partners", "Partners", "Integrations", "The external parties you exchange data with.", + (View, "Browse partners and their properties."), + (Create, "Create partners."), + (Edit, "Change partner details, properties and API keys."), + (Delete, "Delete partners.")), + + Area("documents", "Information types", "Integrations", + "The kinds of business documents that flow between partners.", + (View, "Browse information types."), + (Create, "Create information types."), + (Edit, "Change information types, codes and promoted properties."), + (Delete, "Delete unused information types.")), + + Area("global-values", "Global values", "Integrations", "Shared value sets adapters can reference.", + (View, "Browse global value sets."), + (Create, "Create value sets."), + (Edit, "Change value sets."), + (Delete, "Delete value sets.")), + + Area("notifiers", "Notifiers", "Integrations", "Alerts sent when exchanges fail or succeed.", + (View, "Browse notifiers and their delivery history."), + (Create, "Create notifiers."), + (Edit, "Change notifiers."), + (Delete, "Remove notifiers.")), + + Area("api-gateways", "API gateways", "Integrations", "HTTP entry points partners call into.", + (View, "Browse API gateways and attached partners."), + (Create, "Create new API gateways."), + (Edit, "Change gateways and partner attachments."), + (Delete, "Delete API gateways.")), + + Area("bus-gateways", "Bus gateways", "Integrations", "Bus listeners that route documents to integrations.", + (View, "Browse bus gateways and routes."), + (Create, "Create new bus gateways."), + (Edit, "Change gateways and routes."), + (Delete, "Delete bus gateways.")), + + // ——— Configuration ——— + Area("workgroups", "Work groups", "Configuration", "Processing lanes that spread load across queues.", + (View, "See work groups and their throughput."), + (Create, "Create work groups."), + (Edit, "Change work group settings."), + (Delete, "Delete unused work groups.")), + + Area("retry-policies", "Retry policies", "Configuration", "Rules for retrying failed exchanges.", + (View, "Browse retry policies and scheduled retries."), + (Create, "Create retry policies."), + (Edit, "Change retry policies."), + (Delete, "Delete retry policies.")), + + // ——— Administration ——— + Area("users", "Members", "Administration", "The people who can sign in to this Bitween instance.", + (View, "See the member list."), + (Create, "Invite new members."), + (Edit, "Change members' roles, disable accounts, reset passwords."), + (Delete, "Remove members.")), + + Area("roles", "Roles", "Administration", "What each kind of member is allowed to do.", + (View, "See roles and their permissions."), + (Create, "Create roles."), + (Edit, "Change role permissions."), + (Delete, "Delete unassigned roles.")), + + Area("settings", "Settings", "Administration", "Instance-wide configuration.", + (View, "See instance settings."), + (Edit, "Change instance settings.")) + ]; + + /// Every valid permission key. + public static readonly HashSet AllKeys = + Areas.SelectMany(a => a.Actions.Select(x => $"{a.Id}.{x.Id}")).ToHashSet(); + + /// Drops anything not in the catalog — stale keys left over from a removed area. + public static List Sanitize(IEnumerable keys) => + (keys ?? []).Where(AllKeys.Contains).Distinct().ToList(); + + /// Every key in the given navigation groups, optionally view-only. + public static List InGroups(bool viewOnly, params string[] groups) => + Areas.Where(a => groups.Contains(a.Group)) + .SelectMany(a => a.Actions.Where(x => !viewOnly || x.Id == View).Select(x => $"{a.Id}.{x.Id}")) + .ToList(); +} diff --git a/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs b/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs new file mode 100644 index 00000000..7e68f8f7 --- /dev/null +++ b/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs @@ -0,0 +1,39 @@ +using System; + +namespace SW.Bitween.Model; + +/// +/// The JSON handed to an alert handler when a retry group's shared budget runs out for one +/// subscription, meaning failures matching that group have stopped being retried. +/// +/// +/// Sent once per subscription and group, and not again until the budget is reset — unlike +/// , which is sent per exchange. +/// +public class RetryBudgetExhaustedNotification +{ + /// The failure that found the budget empty. + public string XchangeId { get; set; } + + public int SubscriptionId { get; set; } + public string SubscriptionName { get; set; } + public string DocumentName { get; set; } + public string CorrelationId { get; set; } + + /// Null when the subscription uses an inline policy rather than a named one. + public string PolicyName { get; set; } + + /// The group whose budget is spent — the condition that has stopped being retried. + public string GroupName { get; set; } + + /// The ceiling that was reached. + public int MaxAttemptsTotal { get; set; } + + /// The policy's own words for why this failure was refused. + public string BlockedReason { get; set; } + + /// The failure text of the exchange that hit the empty budget. + public string Exception { get; set; } + + public DateTime OccurredOn { get; set; } +} diff --git a/SW.Bitween.Sdk/Model/RetryPolicyModel.cs b/SW.Bitween.Sdk/Model/RetryPolicyModel.cs index 5d617542..20983ecf 100644 --- a/SW.Bitween.Sdk/Model/RetryPolicyModel.cs +++ b/SW.Bitween.Sdk/Model/RetryPolicyModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace SW.Bitween.Model; @@ -6,6 +7,15 @@ public class RetryPolicyCreate { public required string Name { get; set; } public List Groups { get; set; } = []; + + /// + /// Default destination for budget-exhausted alerts, inherited by every group that does not + /// override it. Null means no alert unless a group or a subscription+group override sets one. + /// + public string? AlertHandlerId { get; set; } + + /// That adapter's own settings — api key, recipients, subject. + public Dictionary? AlertHandlerProperties { get; set; } } public class RetryPolicyUpdate : RetryPolicyCreate { } @@ -17,6 +27,185 @@ public class RetryPolicyRow public int GroupCount { get; set; } } +/// +/// The whole state of one subscription-and-group pair under a policy: how much of the group's +/// that subscription has spent, and where the pair's +/// budget-exhausted alert goes. +/// +/// +/// +/// Both halves are keyed by the same (SubscriptionId, GroupId) pair, which is why they +/// travel together rather than in two reports the reader has to join by eye: the question asked +/// when a budget runs out is "did anyone get told?", and that needs both. +/// +/// +/// A row exists for every pair, including subscriptions that have never failed — an alert override +/// has to be configurable before the first failure, not after. Those rows carry the group's ceiling +/// with nothing spent against it, and a null . +/// +/// +public class RetryGroupUsageRow +{ + public int SubscriptionId { get; set; } + public string SubscriptionName { get; set; } + public Guid GroupId { get; set; } + public string GroupName { get; set; } + + public int AttemptsUsed { get; set; } + public int MaxAttemptsTotal { get; set; } + + /// True when the budget is spent and this subscription will get no further retries. + public bool Exhausted { get; set; } + + /// + /// Null when this pair has never failed, which is also how a caller knows there is no counter + /// to reset for it. + /// + public DateTime? LastAttemptOn { get; set; } + + /// + /// When the exhaustion alert was raised, or null if the budget still has room — or ran out + /// before alerts existed. + /// + public DateTime? ExhaustedNotifiedOn { get; set; } + + /// + /// Whether the raised alert actually reached its handler. + /// + /// + /// Separate from because they are different facts: + /// claiming the alert is what stops it firing twice, and it is claimed before the send is + /// attempted. A send through a customer-configured adapter can fail — a wrong password, a + /// refused TLS handshake — and the counter records none of that. Reporting only the claim + /// tells the reader someone was told when nobody was, which is the one thing this page + /// must never do. + /// + /// Null when no alert has been claimed, or when one was claimed with no delivery attempt + /// recorded against it. "Not known" and "did not arrive" are not the same answer. + /// + public bool? AlertDelivered { get; set; } + + /// Why delivery failed, when it did. + public string? AlertError { get; set; } + + /// This pair's own override mode. Inherit when no override row exists. + public RetryAlertMode AlertMode { get; set; } + + /// The override's handler, when it defines one. Not the resolved handler. + public string? OverrideHandlerId { get; set; } + + /// That override's own settings. + public Dictionary? OverrideHandlerProperties { get; set; } + + /// Where the alert actually goes, or null when nothing sends for this pair. + public string? ResolvedHandlerId { get; set; } + + /// + /// The winning level's own settings. Carried so that overriding an inherited alert can start + /// from what it currently sends: an override replaces rather than merges, so a handler copied + /// without its properties would save an override that fails at send time. + /// + public Dictionary? ResolvedHandlerProperties { get; set; } + + /// Which level supplied , or null when nothing sends. + public RetryAlertLevel? ResolvedFrom { get; set; } + + /// + /// Which level deliberately switched this pair's alert off, when one did. Resolution returns + /// nothing in that case exactly as it does when no level ever configured an alert, and the two + /// need telling apart: one is a decision, the other is an oversight. + /// + public RetryAlertLevel? SilencedAt { get; set; } +} + +/// Asks for one subscription-and-group pair's most recent failures. +public class RetryGroupAttemptsRequest +{ + public int SubscriptionId { get; set; } + public Guid GroupId { get; set; } +} + +/// +/// The failures one group caught for one subscription: what the spent budget on a +/// was actually spent on. +/// +/// +/// Failures are kept for good, while the budget counter is reset, so counts +/// every failure this group has ever caught for this subscription and not the counter's value. +/// Failures recorded before a group was stamped onto them are not counted at all. +/// +public class RetryGroupAttempts +{ + /// How many failures exist, of which carries the latest few. + public int Total { get; set; } + + public List Attempts { get; set; } = []; +} + +public class RetryGroupAttemptRow +{ + /// The failed exchange, so the full input, output and error can be opened. + public string XchangeId { get; set; } + + /// + /// How deep the retry chain was, 0 being the original delivery. Null for failures recorded + /// before the number was stored. + /// + public int? AttemptNumber { get; set; } + + public DateTime FailedOn { get; set; } + + public string Exception { get; set; } + + /// + /// True while another attempt is still scheduled for this failure. The one thing here that is + /// not history: it stops being true the moment the retry runs. + /// + public bool RetryPending { get; set; } + + /// Why no further attempt was scheduled, when the policy refused one. + public string RetryBlockedReason { get; set; } +} + +/// +/// Creates, changes or clears the alert override for one subscription and group. Sending +/// removes the override rather than storing a row that does +/// nothing. +/// +public class RetryAlertOverrideSave +{ + public int SubscriptionId { get; set; } + public Guid GroupId { get; set; } + public RetryAlertMode AlertMode { get; set; } + public string? AlertHandlerId { get; set; } + public Dictionary? AlertHandlerProperties { get; set; } +} + +/// Empty request body — the subject is identified by the route key. +public class RetryPolicyUsageRequest +{ +} + +/// +/// Clears one subscription's spent budget, for one group or for all of them. Reaches a subscription +/// whose policy is an inline CustomRetryPolicy, which has no id for the policy-scoped reset to +/// address. +/// +public class SubscriptionRetryResetUsage +{ + public Guid? GroupId { get; set; } +} + +/// +/// Clears spent budget so a group starts retrying again. Omit both fields to reset every +/// subscription and group of the policy. +/// +public class RetryPolicyResetUsage +{ + public int? SubscriptionId { get; set; } + public Guid? GroupId { get; set; } +} + /// /// Simulates evaluating a (possibly unsaved/draft) set of retry groups against a single /// failure, across as many consecutive attempts as requested, so the management UI can diff --git a/SW.Bitween.Sdk/Model/Roles.cs b/SW.Bitween.Sdk/Model/Roles.cs new file mode 100644 index 00000000..22b5f7f4 --- /dev/null +++ b/SW.Bitween.Sdk/Model/Roles.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace SW.Bitween.Model; + +public class RoleCreate +{ + public required string Name { get; set; } + public string Description { get; set; } + public List Permissions { get; set; } = []; +} + +public class RoleUpdate : RoleCreate; + +/// One shape for both the roles list and a single role — the UI shows the same fields. +public class RoleRow +{ + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + /// Built-in roles can be assigned, but not edited or deleted. + public bool IsSystem { get; set; } + + public List Permissions { get; set; } = []; + public int MemberCount { get; set; } + public DateTime CreatedOn { get; set; } +} diff --git a/SW.Bitween.Sdk/Model/Settings.cs b/SW.Bitween.Sdk/Model/Settings.cs new file mode 100644 index 00000000..3e69c42a --- /dev/null +++ b/SW.Bitween.Sdk/Model/Settings.cs @@ -0,0 +1,51 @@ +namespace SW.Bitween.Model; + +/// +/// One setting as the settings page sees it: the catalog's definition plus whatever value is in +/// effect. A secret's value never leaves the server — only reveals that +/// one is set. +/// +public class SettingRow +{ + public string Key { get; set; } + public string Section { get; set; } + public string Label { get; set; } + public string Description { get; set; } + + /// "string", "number", "boolean" or "color". + public string Kind { get; set; } + + /// The product default — what a reset returns this setting to. Empty for secrets. + public string DefaultValue { get; set; } + + /// The stored value. Always null for secrets, whose value never leaves the server. + public string Value { get; set; } + + public bool Secret { get; set; } + + /// True when the stored value differs from the product default, i.e. a reset would do something. + public bool Overridden { get; set; } + + /// Whether the effective value is non-empty — lets the UI mask a secret that is set. + public bool HasValue { get; set; } + + /// + /// Whether the UI may write this setting. False for every environment-owned setting, and for a + /// secret on an instance with no Bitween:SettingsEncryptionKey — there's nowhere safe to + /// store that, so it stays configuration-only. + /// + public bool Editable { get; set; } + + /// + /// How to render the row: "editable" (stored, changeable), "readonly" (an + /// environment value, shown but not changeable) or "presence" (an environment value + /// reported only as set or not set). + /// + public string Access { get; set; } +} + +public class SettingUpdate +{ + /// The new value as text; empty clears the setting. Reset-to-default is a DELETE instead. + public string Value { get; set; } +} diff --git a/SW.Bitween.Sdk/Model/Subscription.cs b/SW.Bitween.Sdk/Model/Subscription.cs index 17943c53..5211a967 100644 --- a/SW.Bitween.Sdk/Model/Subscription.cs +++ b/SW.Bitween.Sdk/Model/Subscription.cs @@ -45,27 +45,135 @@ public class SearchSubscriptionTrailModel public int SubscriptionId { get; set; } } - public abstract class SubscriptionCreateUpdateBase : IName + // One execution of a scheduled subscription, out of the scheduler's own history. + // Only Receiving and Aggregation subscriptions run on a schedule, so only they + // have runs; everything else returns an empty list. + public class SubscriptionRunModel { - public string Name { get; set; } - public int DocumentId { get; set; } - public int? PartnerId { get; set; } - public int? AggregationForId { get; set; } + public DateTime StartedOn { get; set; } + public DateTime? EndedOn { get; set; } + public long? DurationMs { get; set; } + + /// Null while the run is still in progress. + public bool? Success { get; set; } + public string Error { get; set; } + public string Node { get; set; } + + /// True when someone pressed Receive now / Aggregate now instead of waiting for the cron. + public bool Manual { get; set; } } - public class SubscriptionCreate : SubscriptionCreateUpdateBase + public class SubscriptionLastRunModel : SubscriptionRunModel { - public SubscriptionType Type { get; set; } + public int SubscriptionId { get; set; } + + /// Finished runs in the recent window — in-progress runs are excluded, being neither. + public int RecentTotal { get; set; } + + /// How many of succeeded. + public int RecentSucceeded { get; set; } } - public class SubscriptionSearch : SubscriptionGet + public class SearchSubscriptionRunsModel + { + public int? Limit { get; set; } + public int SubscriptionId { get; set; } + } + + public class SearchSubscriptionLastRunsModel + { + } + + /// + /// One execution of a Receiving subscription's own receive step, recorded directly by + /// ReceivingJob — independent of the scheduler's own run history, which only knows + /// whether Execute() threw (it never does; the receive step's own failures are caught + /// and reported here instead, alongside the successes and no-op checks history never covers). + /// + public enum ReceiveOutcome + { + Failed = 0, + NoNewData = 1, + Received = 2, + } + + public class ReceiveAttemptExchangeRef + { + public string Id { get; set; } + public bool? Status { get; set; } + public bool? ResponseBad { get; set; } + public IDictionary PromotedProperties { get; set; } + } + + public class ReceiveAttemptModel { public int Id { get; set; } - public string DocumentName { get; set; } - public bool? IsRunning { get; set; } + public DateTime StartedOn { get; set; } + public DateTime FinishedOn { get; set; } + public ReceiveOutcome Outcome { get; set; } + public string ErrorMessage { get; set; } + public ICollection Exchanges { get; set; } } - public class SubscriptionUpdate : SubscriptionCreateUpdateBase + public class SearchReceiveAttemptsModel + { + public int SubscriptionId { get; set; } + public ReceiveOutcome? Outcome { get; set; } + public int? Offset { get; set; } + public int? Limit { get; set; } + } + + /// + /// Whether a scheduled subscription will actually fire — read from the scheduler + /// itself, not from what Bitween thinks it configured. The two can disagree, and + /// when they do it is silent: the UI shows a healthy job that never runs. + /// + public class SubscriptionScheduleHealthModel + { + public int SubscriptionId { get; set; } + + /// Schedules configured on the subscription. + public int ScheduleCount { get; set; } + + /// Triggers the scheduler actually holds. Fewer than ScheduleCount means some schedule will never fire. + public int TriggerCount { get; set; } + + /// Worst state across the subscription's triggers: Normal, Paused, Blocked, Error, Complete, or Missing. + public string State { get; set; } + + /// The scheduler's own next fire time — computed from the cron, independently of Subscription.ReceiveOn. + public DateTime? NextFireOn { get; set; } + + /// + /// The subscription is flagged as running but the scheduler has nothing executing for it. + /// Left behind when a run is killed rather than thrown: the flag is never cleared and + /// every later fire is skipped by the concurrency guard, so the job silently stops. + /// + public bool Stuck { get; set; } + } + + public class SearchSubscriptionScheduleHealthModel + { + } + + public abstract class SubscriptionCreateUpdateBase : IName + { + public string Name { get; set; } + public int DocumentId { get; set; } + public int? PartnerId { get; set; } + public int? AggregationForId { get; set; } + } + + /// + /// How a subscription is configured — everything a person chooses. Shared by create and + /// update so the two can't drift: a field here is applied by both, through the same code. + /// + /// Runtime state (what the subscription has since done — failure counts, last exception, + /// next fire time) deliberately lives on only. None of it + /// is meaningful for something that doesn't exist yet. + /// + /// + public abstract class SubscriptionConfiguration : SubscriptionCreateUpdateBase { public string HandlerId { get; set; } public string MapperId { get; set; } @@ -74,7 +182,6 @@ public class SubscriptionUpdate : SubscriptionCreateUpdateBase public int? CategoryId { get; set; } public int? WorkGroupId { get; set; } - public bool Temporary { get; set; } public IPropertyMatchSpecification MatchExpression { get; set; } public ICollection HandlerProperties { get; set; } public ICollection ValidatorProperties { get; set; } @@ -82,13 +189,57 @@ public class SubscriptionUpdate : SubscriptionCreateUpdateBase public ICollection ReceiverProperties { get; set; } public ICollection DocumentFilter { get; set; } - public bool Inactive { get; set; } - - //public ICollection AggregationSchedules { get; set; } public ICollection Schedules { get; set; } public int? ResponseSubscriptionId { get; set; } public string ResponseMessageTypeName { get; set; } + public int? RetryPolicyId { get; set; } + public CustomRetryPolicy CustomRetryPolicy { get; set; } + } + + /// + /// Creates a subscription complete, in one transaction. Everything beyond + /// and is optional — a caller + /// that sends only those still gets the empty, inactive subscription it always did. + /// + /// + /// An integration defined while it is being wired up, so the integration and the thing + /// that points at it land in one transaction instead of two calls that can half-succeed. + /// + /// Deriving from is the point: the whole pipeline + /// is applied by the same code an ordinary create uses. The type is always the gateway's. + /// DocumentId is ignored for a bus gateway, which is bound to one information type and + /// imposes it; an API gateway is not bound to one, so there it is required. + /// + /// + public class InlineIntegrationCreate : SubscriptionConfiguration + { + } + + public class SubscriptionCreate : SubscriptionConfiguration + { + public SubscriptionType Type { get; set; } + + /// + /// Null (the default) means born inactive, as subscriptions always have been. Pass + /// false to have it live the moment it exists. Nullable on purpose: a plain bool + /// would silently activate every caller that doesn't mention it. + /// + public bool? Inactive { get; set; } + } + + public class SubscriptionSearch : SubscriptionGet + { + public int Id { get; set; } + public string DocumentName { get; set; } + public bool? IsRunning { get; set; } + } + + public class SubscriptionUpdate : SubscriptionConfiguration + { + public bool Temporary { get; set; } + public bool Inactive { get; set; } + public DateTime? ReceiveOn { get; set; } public DateTime? AggregateOn { get; set; } public int ConsecutiveFailures { get; set; } @@ -97,9 +248,6 @@ public class SubscriptionUpdate : SubscriptionCreateUpdateBase public DateTime? PausedOn { get; set; } public string CategoryCode { get; set; } public string CategoryDescription { get; set; } - - public int? RetryPolicyId { get; set; } - public CustomRetryPolicy CustomRetryPolicy { get; set; } } public class SubscriptionGet : SubscriptionUpdate diff --git a/SW.Bitween.Sdk/Model/Workgroups.cs b/SW.Bitween.Sdk/Model/Workgroups.cs index 969a9935..1cc88d44 100644 --- a/SW.Bitween.Sdk/Model/Workgroups.cs +++ b/SW.Bitween.Sdk/Model/Workgroups.cs @@ -26,6 +26,8 @@ public class WorkGroupModel public double? NotifierIncomingRate { get; set; } public long? NotifierProcessingCount { get; set; } public long? NotifierQueueCount { get; set; } + /// Live count of active RabbitMQ consumer instances for this group's queue. + public long? ProcessorNodeCount { get; set; } } public class CreateWorkGroupModel @@ -39,6 +41,7 @@ public class SearchWorkGroupModel { public int? Limit { get; set; } public int? Offset { get; set; } + public string Name { get; set; } } public class UpdateWorkGroupModel : CreateWorkGroupModel diff --git a/SW.Bitween.Sdk/Model/Xchange.cs b/SW.Bitween.Sdk/Model/Xchange.cs index fa321a61..e29dbbc8 100644 --- a/SW.Bitween.Sdk/Model/Xchange.cs +++ b/SW.Bitween.Sdk/Model/Xchange.cs @@ -97,5 +97,8 @@ public class XchangeRow public string CorrelationId { get; set; } public int? PartnerId { get; set; } public DateTime? ScheduledRetryOn { get; set; } + + /// Why the retry policy declined to schedule another attempt, when it declined. + public string RetryBlockedReason { get; set; } } } \ No newline at end of file diff --git a/SW.Bitween.Sdk/SW.Bitween.Sdk.csproj b/SW.Bitween.Sdk/SW.Bitween.Sdk.csproj index 80b9e875..233fc592 100644 --- a/SW.Bitween.Sdk/SW.Bitween.Sdk.csproj +++ b/SW.Bitween.Sdk/SW.Bitween.Sdk.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 SimplyWorks.Bitween.Sdk SimplyWorks.Bitween.Sdk Simplify9 diff --git a/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs b/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs new file mode 100644 index 00000000..ec0528b9 --- /dev/null +++ b/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using SW.Bitween.Model; +using SW.Bitween.NativeAdapters; +using SW.Bitween.NativeAdapters.SmtpHandler; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class NativeSmtpHandlerTests +{ + // ─── Subject and body templating ──────────────────────────────────────────── + + [TestMethod] + public void Fill_SubstitutesPayloadFields() + { + var payload = JsonConvert.SerializeObject(new {GroupName = "timeouts", MaxAttemptsTotal = 8}); + + var result = NativeSmtpHandler.Fill("{{ GroupName }} used all {{ MaxAttemptsTotal }} retries", payload); + + Assert.AreEqual("timeouts used all 8 retries", result); + } + + [TestMethod] + public void Fill_RendersTheRealAlertPayload() + { + // The shape RetryAlertService actually sends, serialised the same way (PascalCase). + var payload = JsonConvert.SerializeObject(new RetryBudgetExhaustedNotification + { + SubscriptionName = "QA - ShipaDelivery - CreateOrder", + GroupName = "FRT charges cannot be found", + MaxAttemptsTotal = 8 + }); + + var result = NativeSmtpHandler.Fill( + "Retries stopped for {{ SubscriptionName }}: {{ GroupName }} ({{ MaxAttemptsTotal }})", payload); + + Assert.AreEqual( + "Retries stopped for QA - ShipaDelivery - CreateOrder: FRT charges cannot be found (8)", result); + } + + [TestMethod] + public void Fill_LeavesTemplateAloneForNonJsonPayload() + { + // Normal for a pipeline handler shipping a flat file — there are no fields to substitute. + Assert.AreEqual("Nightly export", NativeSmtpHandler.Fill("Nightly export", "id,name\n1,alpha")); + } + + [TestMethod] + public void Fill_LeavesTemplateAloneForEmptyPayload() + { + Assert.AreEqual("Nightly export", NativeSmtpHandler.Fill("Nightly export", "")); + } + + [TestMethod] + public void Fill_MissingFieldRendersEmptyRatherThanThePlaceholder() + { + var result = NativeSmtpHandler.Fill("Group: {{ GroupName }}", "{\"Other\":1}"); + + Assert.AreEqual("Group: ", result); + } + + // ─── Startup values ───────────────────────────────────────────────────────── + + [TestMethod] + public void StartupValues_ParseNumbersAndFlags() + { + var input = new Dictionary + { + ["Host"] = "smtp.example.com", + ["Port"] = "465", + ["UseTls"] = "true", + ["IsHtml"] = "false", + ["From"] = "alerts@example.com", + ["To"] = "ops@example.com" + }.ConvertTo(); + + Assert.AreEqual("smtp.example.com", input.Host); + Assert.AreEqual(465, input.Port); + Assert.IsTrue(input.UseTls); + Assert.IsFalse(input.IsHtml); + } + + [TestMethod] + public void StartupValues_KeepDefaultsWhenOmitted() + { + var input = new Dictionary + { + ["Host"] = "smtp.example.com", + ["From"] = "alerts@example.com", + ["To"] = "ops@example.com" + }.ConvertTo(); + + // The common provider setup should need no port or TLS choice at all. + Assert.AreEqual(587, input.Port); + Assert.IsTrue(input.UseTls); + Assert.IsTrue(input.IsHtml); + Assert.IsNull(input.Password); + } + + // ─── Server certificate acceptance ────────────────────────────────────────── + + [TestMethod] + public void Certificate_WithNothingWrong_IsAccepted() + { + Assert.IsTrue(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.None, new[] { X509ChainStatusFlags.NoError })); + } + + [TestMethod] + public void Certificate_WhoseRevocationCouldNotBeChecked_IsAccepted() + { + // The whole point of the soft-fail: the CA's OCSP or CRL server was unreachable, which says + // nothing bad about the certificate itself. + Assert.IsTrue(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.RevocationStatusUnknown })); + + Assert.IsTrue(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.OfflineRevocation })); + + Assert.IsTrue(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.RevocationStatusUnknown | X509ChainStatusFlags.OfflineRevocation })); + } + + [TestMethod] + public void Certificate_ThatWasRevoked_IsRefused() + { + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.Revoked })); + } + + [TestMethod] + public void Certificate_RevokedAlongsideAnUncheckableStatus_IsRefused() + { + // One chain entry can carry several flags at once, so the tolerated ones have to be masked + // out rather than compared — otherwise a revoked certificate rides in on the same entry. + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.Revoked | X509ChainStatusFlags.RevocationStatusUnknown })); + + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.RevocationStatusUnknown, X509ChainStatusFlags.Revoked })); + } + + [TestMethod] + public void Certificate_WithAnyOtherDefect_IsRefused() + { + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.UntrustedRoot })); + + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.NotTimeValid })); + + // A wrong hostname or no certificate at all is not a chain question, so the chain flags must + // not be allowed to excuse it. + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateNameMismatch, + new[] { X509ChainStatusFlags.NoError })); + + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateNotAvailable, + new[] { X509ChainStatusFlags.NoError })); + + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch, + new[] { X509ChainStatusFlags.RevocationStatusUnknown })); + } +} diff --git a/SW.Bitween.UnitTests/PermissionCatalogTests.cs b/SW.Bitween.UnitTests/PermissionCatalogTests.cs new file mode 100644 index 00000000..aa313cbc --- /dev/null +++ b/SW.Bitween.UnitTests/PermissionCatalogTests.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class PermissionCatalogTests +{ + /// Every key declared as a constant on . + private static List ConstantKeys() => + typeof(Permissions).GetNestedTypes() + .SelectMany(area => area.GetFields(BindingFlags.Public | BindingFlags.Static)) + .Where(f => f.IsLiteral && f.FieldType == typeof(string)) + .Select(f => (string)f.GetRawConstantValue()) + .ToList(); + + [TestMethod] + public void Constants_AndCatalog_DescribeTheSameKeys() + { + var constants = ConstantKeys(); + + // A constant with no catalog entry can never be granted, so the handler guarding on it + // would deny everyone. A catalog entry with no constant is a grant nothing enforces. + CollectionAssert.AreEquivalent( + PermissionCatalog.AllKeys.ToList(), + constants, + "Permissions constants and PermissionCatalog have drifted apart."); + } + + [TestMethod] + public void EveryKey_IsAreaDotAction() + { + foreach (var key in PermissionCatalog.AllKeys) + Assert.AreEqual(2, key.Split('.').Length, $"'{key}' is not in . form."); + } + + [TestMethod] + public void Constants_HaveNoDuplicates() + { + var constants = ConstantKeys(); + CollectionAssert.AreEquivalent(constants.Distinct().ToList(), constants); + } + + [TestMethod] + public void Administrator_GrantsEveryPermission() + { + // Derived from the catalog rather than stored, so a newly added permission reaches + // administrators without a migration. Guards that behaviour. + CollectionAssert.AreEquivalent( + PermissionCatalog.AllKeys.ToList(), + Role.SystemPermissions(Role.AdministratorId)); + } + + [TestMethod] + public void MemberAndViewer_CannotReachAdministration() + { + var administration = PermissionCatalog.Areas + .Where(a => a.Group == "Administration") + .SelectMany(a => a.Actions.Select(x => $"{a.Id}.{x.Id}")) + .ToList(); + + foreach (var roleId in new[] { Role.MemberId, Role.ViewerId }) + { + var granted = Role.SystemPermissions(roleId); + Assert.IsFalse(granted.Intersect(administration).Any(), + $"Built-in role {roleId} must not grant members/roles/settings access."); + Assert.IsTrue(granted.Count > 0); + } + } + + [TestMethod] + public void Viewer_GrantsViewOnly() + { + Assert.IsTrue(Role.SystemPermissions(Role.ViewerId).All(k => k.EndsWith(".view"))); + } + + [TestMethod] + public void Member_CanWriteIntegrationsButNotOnlyView() + { + var granted = Role.SystemPermissions(Role.MemberId); + Assert.IsTrue(granted.Contains(Permissions.Subscriptions.Edit)); + Assert.IsTrue(granted.Contains(Permissions.Subscriptions.View)); + Assert.IsFalse(granted.Contains(Permissions.Users.View)); + } + + [TestMethod] + public void Sanitize_DropsKeysOutsideTheCatalog() + { + var cleaned = PermissionCatalog.Sanitize([ + Permissions.Partners.View, "partners.teleport", "", Permissions.Partners.View + ]); + + CollectionAssert.AreEqual(new List { Permissions.Partners.View }, cleaned); + } + + [TestMethod] + public void CustomRole_GrantsExactlyWhatItStores() + { + var role = new Role("Support", "Reads exchanges", [Permissions.Exchanges.View, "nope.view"]); + CollectionAssert.AreEqual(new List { Permissions.Exchanges.View }, + role.GetEffectivePermissions()); + } +} diff --git a/SW.Bitween.UnitTests/RetryAlertResolverTests.cs b/SW.Bitween.UnitTests/RetryAlertResolverTests.cs new file mode 100644 index 00000000..c2a2a29d --- /dev/null +++ b/SW.Bitween.UnitTests/RetryAlertResolverTests.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class RetryAlertResolverTests +{ + // ─── Helpers ──────────────────────────────────────────────────────────────── + + private static RetryGroup Group(RetryAlertMode mode = RetryAlertMode.Inherit, string handler = null) => + new() + { + Name = "timeouts", + AppliesTo = [XchangeResultType.Error], + AlertMode = mode, + AlertHandlerId = handler, + AlertHandlerProperties = handler == null ? null : new Dictionary { ["to"] = "group@x" } + }; + + private static RetryPolicy Policy(string handler = null) => new() + { + Name = "policy", + AlertHandlerId = handler, + AlertHandlerProperties = handler == null ? null : new Dictionary { ["to"] = "policy@x" } + }; + + private static RetryAlertOverride Override(RetryAlertMode mode, string handler = null) => new() + { + SubscriptionId = 1, + AlertMode = mode, + AlertHandlerId = handler, + AlertHandlerProperties = handler == null ? null : new Dictionary { ["to"] = "sub@x" } + }; + + // ─── Nothing configured ───────────────────────────────────────────────────── + + [TestMethod] + public void NoLevelConfigured_ResolvesToNothing() + { + Assert.IsNull(RetryAlertResolver.Resolve(null, Group(), Policy())); + } + + // ─── Policy level ─────────────────────────────────────────────────────────── + + [TestMethod] + public void PolicyOnly_ResolvesToPolicy() + { + var target = RetryAlertResolver.Resolve(null, Group(), Policy("native.smtp")); + + Assert.IsNotNull(target); + Assert.AreEqual("native.smtp", target.HandlerId); + Assert.AreEqual(RetryAlertLevel.Policy, target.Level); + Assert.AreEqual("policy@x", target.HandlerProperties["to"]); + } + + // ─── Group level ──────────────────────────────────────────────────────────── + + [TestMethod] + public void GroupSend_ReplacesPolicyEntirely() + { + var target = RetryAlertResolver.Resolve(null, + Group(RetryAlertMode.Send, "native.teams"), Policy("native.smtp")); + + Assert.AreEqual("native.teams", target.HandlerId); + Assert.AreEqual(RetryAlertLevel.Group, target.Level); + // Replace, not merge: nothing of the policy's own properties survives. + Assert.AreEqual("group@x", target.HandlerProperties["to"]); + } + + [TestMethod] + public void GroupSilent_SuppressesPolicyAlert() + { + Assert.IsNull(RetryAlertResolver.Resolve(null, + Group(RetryAlertMode.Silent), Policy("native.smtp"))); + } + + [TestMethod] + public void GroupInherit_FallsThroughToPolicy() + { + var target = RetryAlertResolver.Resolve(null, + Group(RetryAlertMode.Inherit), Policy("native.smtp")); + + Assert.AreEqual(RetryAlertLevel.Policy, target.Level); + } + + // ─── Subscription + group level ───────────────────────────────────────────── + + [TestMethod] + public void SubscriptionOverrideSend_WinsOverGroupAndPolicy() + { + var target = RetryAlertResolver.Resolve( + Override(RetryAlertMode.Send, "native.webhook"), + Group(RetryAlertMode.Send, "native.teams"), + Policy("native.smtp")); + + Assert.AreEqual("native.webhook", target.HandlerId); + Assert.AreEqual(RetryAlertLevel.SubscriptionGroup, target.Level); + Assert.AreEqual("sub@x", target.HandlerProperties["to"]); + } + + [TestMethod] + public void SubscriptionOverrideSilent_SuppressesEverythingAbove() + { + Assert.IsNull(RetryAlertResolver.Resolve( + Override(RetryAlertMode.Silent), + Group(RetryAlertMode.Send, "native.teams"), + Policy("native.smtp"))); + } + + [TestMethod] + public void SubscriptionOverrideInherit_FallsThroughToGroup() + { + var target = RetryAlertResolver.Resolve( + Override(RetryAlertMode.Inherit), + Group(RetryAlertMode.Send, "native.teams"), + Policy("native.smtp")); + + Assert.AreEqual(RetryAlertLevel.Group, target.Level); + } + + // ─── Edge cases ───────────────────────────────────────────────────────────── + + [TestMethod] + public void InlineCustomPolicy_HasNoPolicyLevel_ButGroupStillSends() + { + // A subscription with a CustomRetryPolicy has no policy row at all. + var target = RetryAlertResolver.Resolve(null, Group(RetryAlertMode.Send, "native.teams"), null); + + Assert.AreEqual(RetryAlertLevel.Group, target.Level); + } + + [TestMethod] + public void InlineCustomPolicy_WithInheritingGroup_ResolvesToNothing() + { + Assert.IsNull(RetryAlertResolver.Resolve(null, Group(), null)); + } + + [TestMethod] + public void MissingGroup_StillFallsBackToPolicy() + { + // The group was removed from the policy between the failure and the send. + var target = RetryAlertResolver.Resolve(null, null, Policy("native.smtp")); + + Assert.AreEqual(RetryAlertLevel.Policy, target.Level); + } + + [TestMethod] + public void SendWithNoHandler_FallsThroughRatherThanSilencing() + { + // Validation rejects this on save, so it only exists on rows written before that guard. + // Falling through is more useful than silently sending nothing. + var target = RetryAlertResolver.Resolve( + Override(RetryAlertMode.Send), + Group(RetryAlertMode.Send), + Policy("native.smtp")); + + Assert.AreEqual(RetryAlertLevel.Policy, target.Level); + } +} diff --git a/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs b/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs index bb8a11a5..23fa42c5 100644 --- a/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs +++ b/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using SW.Bitween.Model; @@ -57,11 +58,41 @@ private static RetryGroup BadResultGroup( } }; + // Each call gets its own budget, so tests that don't care about the shared total + // stay isolated from each other. + private static RetryPolicyEvaluator Evaluator(IRetryPolicy policy) => + new RetryPolicyEvaluator(policy, new InMemoryRetryGroupBudget()); + private sealed class TestPolicy(RetryGroup[] groups) : IRetryPolicy { public List Groups { get; } = new List(groups); } + // ─── Allow with no budget ─────────────────────────────────────────────────── + + [TestMethod] + public async Task AllowWithoutBudget_IsRefusedWithAReason() + { + // Only reachable for a policy saved before validation rejected this shape. It used to throw, + // and the caller logs and swallows the throw, so retries stopped happening and nothing said why. + var group = new RetryGroup + { + Name = "No budget", + Priority = 10, + Enabled = true, + AppliesTo = [XchangeResultType.Error], + Action = RetryAction.Allow, + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = null + }; + + var decision = await Evaluator(PolicyWith(group)).Evaluate(XchangeResultType.Error, "timeout", 0); + + Assert.IsFalse(decision.ShouldRetry); + StringAssert.Contains(decision.Reason, "no budget"); + Assert.AreEqual("No budget", decision.MatchedGroup?.Name); + } + // ─── ContainsMatcher ──────────────────────────────────────────────────────── [TestMethod] @@ -218,85 +249,149 @@ public void JsonPathMatcher_ArrayIndexer_Match() // ─── Evaluator: basic routing ──────────────────────────────────────────────── [TestMethod] - public void Evaluator_MatchingGroup_AllowsRetry() + public async Task Evaluator_MatchingGroup_AllowsRetry() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "timeout" })); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "Connection timeout", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "Connection timeout", 0); Assert.IsTrue(decision.ShouldRetry); Assert.AreEqual("transient", decision.MatchedGroupName); } [TestMethod] - public void Evaluator_NoMatchingGroup_Blocks() + public async Task Evaluator_NoMatchingGroup_Blocks() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "timeout" })); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "Disk full", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "Disk full", 0); Assert.IsFalse(decision.ShouldRetry); } [TestMethod] - public void Evaluator_WrongResultType_GroupSkipped() + public async Task Evaluator_WrongResultType_GroupSkipped() { var policy = PolicyWith(BadResultGroup("bad", new JsonPathMatcher { Path = "$.retryable", Op = JsonPathOp.Exists })); - var ev = new RetryPolicyEvaluator(policy); + var ev = Evaluator(policy); // Group is for BadResult only — must be skipped for Error - var decision = ev.Evaluate(XchangeResultType.Error, "some exception", 0); + var decision = await ev.Evaluate(XchangeResultType.Error, "some exception", 0); + Assert.IsFalse(decision.ShouldRetry); + } + + [TestMethod] + public async Task Evaluator_ContainsMatcher_MatchesBadResultBody() + { + var policy = PolicyWith(BadResultGroup("bad", new ContainsMatcher { Value = "INSUFFICIENT_STOCK" })); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.BadResult, "{\"code\":\"INSUFFICIENT_STOCK\"}", 0); + Assert.IsTrue(decision.ShouldRetry); + Assert.AreEqual("bad", decision.MatchedGroupName); + } + + [TestMethod] + public async Task Evaluator_ContainsMatcher_MatchesNonJsonBadResultBody() + { + // A 4xx body need not be JSON — text matchers are the only way to reach these. + var policy = PolicyWith(BadResultGroup("bad", new ContainsMatcher { Value = "rate limit" })); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.BadResult, "Rate limit exceeded", 0); + Assert.IsTrue(decision.ShouldRetry); + } + + [TestMethod] + public async Task Evaluator_RegexMatcher_MatchesBadResultBody() + { + var policy = PolicyWith(BadResultGroup("bad", new RegexMatcher { Pattern = @"""status"":\s*""FAILED""" })); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.BadResult, "{\"status\": \"FAILED\"}", 0); + Assert.IsTrue(decision.ShouldRetry); + } + + [TestMethod] + public async Task Evaluator_ExceptionTypeMatcher_SkippedForBadResult() + { + // Exception type names are meaningless against a response body — stays Error-only. + var policy = PolicyWith(BadResultGroup("bad", new ExceptionTypeMatcher { Value = "System.TimeoutException" })); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.BadResult, "System.TimeoutException in body", 0); Assert.IsFalse(decision.ShouldRetry); } + [TestMethod] + public async Task Evaluator_EmptyMatchers_MatchesEveryApplicableFailure() + { + var group = new RetryGroup + { + Name = "catch-all", + Priority = 10, + Enabled = true, + AppliesTo = [XchangeResultType.Error], + Action = RetryAction.Allow, + Matchers = [], + Budget = new RetryBudget + { + MaxAttemptsPerError = 5, + MaxAttemptsTotal = 100, + DelayStrategy = new FixedDelayStrategy { DelayMs = 1000 } + } + }; + var policy = PolicyWith(group); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "anything at all", 0); + Assert.IsTrue(decision.ShouldRetry); + Assert.AreEqual("catch-all", decision.MatchedGroupName); + } + // ─── Evaluator: priority ordering ─────────────────────────────────────────── [TestMethod] - public void Evaluator_LowerPriorityEvaluatedFirst() + public async Task Evaluator_LowerPriorityEvaluatedFirst() { var g1 = ErrorGroup("low-num", new ContainsMatcher { Value = "error" }, priority: 1); var g2 = ErrorGroup("high-num", new ContainsMatcher { Value = "error" }, priority: 20); var policy = PolicyWith(g2, g1); // intentionally reversed in array - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "error occurred", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "error occurred", 0); Assert.AreEqual("low-num", decision.MatchedGroupName); } [TestMethod] - public void Evaluator_OnlyMatchingGroupFires() + public async Task Evaluator_OnlyMatchingGroupFires() { var g1 = ErrorGroup("timeouts", new ContainsMatcher { Value = "timeout" }, priority: 1); var g2 = ErrorGroup("disk", new ContainsMatcher { Value = "disk" }, priority: 20); var policy = PolicyWith(g1, g2); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "disk full", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "disk full", 0); Assert.AreEqual("disk", decision.MatchedGroupName); } // ─── Evaluator: budget — MaxAttemptsPerError ───────────────────────────────── [TestMethod] - public void Evaluator_MaxAttemptsPerError_BlocksAfterCap() + public async Task Evaluator_MaxAttemptsPerError_BlocksAfterCap() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, maxPerError: 2)); - var ev = new RetryPolicyEvaluator(policy); + var ev = Evaluator(policy); - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 1).ShouldRetry); - Assert.IsFalse(ev.Evaluate(XchangeResultType.Error, "err", 2).ShouldRetry); // cap = 2 + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 1)).ShouldRetry); + Assert.IsFalse((await ev.Evaluate(XchangeResultType.Error, "err", 2)).ShouldRetry); // cap = 2 } // ─── Evaluator: budget — MaxAttemptsTotal ──────────────────────────────────── [TestMethod] - public void Evaluator_MaxAttemptsTotal_BlocksAfterGroupCap() + public async Task Evaluator_MaxAttemptsTotal_BlocksAfterGroupCap() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, maxPerError: 100, maxTotal: 3)); - var ev = new RetryPolicyEvaluator(policy); + var ev = Evaluator(policy); // Three different "messages" (attempt index 0 each time) exhaust the group total - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); - Assert.IsFalse(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exceeded total=3 + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsFalse((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); // exceeded total=3 } // ─── Evaluator: delay strategies ───────────────────────────────────────────── @@ -330,67 +425,74 @@ public void ExponentialDelay_DoublesAndCaps() } [TestMethod] - public void Evaluator_DelayFromStrategy_ReturnsCorrectValue() + public async Task Evaluator_DelayFromStrategy_ReturnsCorrectValue() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, delay: new FixedDelayStrategy { DelayMs = 3000 })); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "err", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "err", 0); Assert.AreEqual(TimeSpan.FromMilliseconds(3000), decision.Delay); } - // ─── Evaluator: GroupAttemptCounts persistence ─────────────────────────────── + // ─── Evaluator: the total cap is shared, not per message ───────────────────── [TestMethod] - public void GroupAttemptCounts_RestoredAcrossEvaluators_ContinuesBudget() + public async Task Evaluator_MaxAttemptsTotal_SharedAcrossSeparateMessages() { + // The bug this covers: one evaluator per failed xchange, each starting from zero, so + // four failing messages × 3 per-message attempts produced 12 retries under a total of 10. var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, - maxPerError: 100, maxTotal: 2)); + maxPerError: 3, maxTotal: 4)); + var budget = new InMemoryRetryGroupBudget(); - var ev1 = new RetryPolicyEvaluator(policy); - Assert.IsTrue(ev1.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // group count = 1 - var counts = ev1.GetGroupAttemptCounts(); - - // Simulate the next evaluator instance restoring saved state - var ev2 = new RetryPolicyEvaluator(policy); - ev2.RestoreGroupAttemptCounts(counts); // group count restored to 1 - Assert.IsTrue(ev2.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // group count = 2 - var counts2 = ev2.GetGroupAttemptCounts(); + var allowed = 0; + for (var message = 0; message < 4; message++) + for (var attempt = 0; attempt < 3; attempt++) + { + // A fresh evaluator per failure, exactly as XchangeService builds one. + var ev = new RetryPolicyEvaluator(policy, budget); + if ((await ev.Evaluate(XchangeResultType.Error, "err", attempt)).ShouldRetry) allowed++; + } - var ev3 = new RetryPolicyEvaluator(policy); - ev3.RestoreGroupAttemptCounts(counts2); // group count restored to 2 - Assert.IsFalse(ev3.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exceeded total=2 + Assert.AreEqual(4, allowed); } [TestMethod] - public void GroupAttemptCounts_WithoutRestore_BudgetResetsToZero() + public async Task Evaluator_MaxAttemptsTotal_SeparateBudgetsDoNotShare() { + // Two integrations pointed at the same policy template get independent totals. var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, maxPerError: 100, maxTotal: 1)); - var ev1 = new RetryPolicyEvaluator(policy); - Assert.IsTrue(ev1.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exhausted - - // Fresh evaluator without restore — budget starts from 0 again - var ev2 = new RetryPolicyEvaluator(policy); - Assert.IsTrue(ev2.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); + Assert.IsTrue((await Evaluator(policy).Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await Evaluator(policy).Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); } [TestMethod] - public void GetGroupAttemptCounts_ReturnsNonEmptyAfterMatch() + public async Task Evaluator_PerMessageCapBlocked_DoesNotSpendSharedTotal() { - var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" })); - var ev = new RetryPolicyEvaluator(policy); - ev.Evaluate(XchangeResultType.Error, "err", 0); - var counts = ev.GetGroupAttemptCounts(); - Assert.IsTrue(counts.Count > 0); + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, + maxPerError: 1, maxTotal: 2)); + var budget = new InMemoryRetryGroupBudget(); + + // Attempt index 1 is past the per-message cap, so it must not claim a slot. + Assert.IsFalse((await new RetryPolicyEvaluator(policy, budget) + .Evaluate(XchangeResultType.Error, "err", 1)).ShouldRetry); + + // Both slots of the total are therefore still there. + Assert.IsTrue((await new RetryPolicyEvaluator(policy, budget) + .Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await new RetryPolicyEvaluator(policy, budget) + .Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsFalse((await new RetryPolicyEvaluator(policy, budget) + .Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); } // ─── Evaluator: Block action ───────────────────────────────────────────────── [TestMethod] - public void Evaluator_BlockAction_NeverRetries() + public async Task Evaluator_BlockAction_NeverRetries() { var blockGroup = new RetryGroup { @@ -403,8 +505,8 @@ public void Evaluator_BlockAction_NeverRetries() Budget = null }; var policy = PolicyWith(blockGroup); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "fatal: cannot recover", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "fatal: cannot recover", 0); Assert.IsFalse(decision.ShouldRetry); } } diff --git a/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj b/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj index 582b0381..d0bd3a45 100644 --- a/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj +++ b/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 latest false SW.Bitween.UnitTests @@ -12,14 +12,14 @@ - - + + - + - + diff --git a/SW.Bitween.UnitTests/ScribanGeneratorParityTests.cs b/SW.Bitween.UnitTests/ScribanGeneratorParityTests.cs index 6ced9280..2ce3363f 100644 --- a/SW.Bitween.UnitTests/ScribanGeneratorParityTests.cs +++ b/SW.Bitween.UnitTests/ScribanGeneratorParityTests.cs @@ -83,7 +83,7 @@ public void Parity_PartnerProperty_RendersCorrectly() { const string template = """ { - "pkey": {{ __partner__?.apiKey | json }}, + "pkey": {{ (__partner__ ?? {})["apiKey"] | json }}, } """; const string inputJson = """{"__partner__": {"apiKey": "secret-123"}}"""; @@ -96,13 +96,31 @@ public void Parity_PartnerProperty_MissingPartner_ReturnsNull() { const string template = """ { - "pkey": {{ __partner__?.apiKey | json }}, + "pkey": {{ (__partner__ ?? {})["apiKey"] | json }}, } """; var result = ScribanJsonTestHelper.RenderObject(template, "{}"); Assert.AreEqual(JTokenType.Null, result["pkey"]?.Type); } + // ── Partner property with a key that isn't a valid bare identifier ───────── + // Config: { target: 'pkey', source: '', partnerPropKey: 'gw prop test' } + // Regression: the old `__partner__?.gw prop test` shape wasn't even legal Scriban for + // a key like this. String-indexing works for any key content. + + [TestMethod] + public void Parity_PartnerProperty_KeyWithSpaces_RendersCorrectly() + { + const string template = """ + { + "pkey": {{ (__partner__ ?? {})["gw prop test"] | json }}, + } + """; + const string inputJson = """{"__partner__": {"gw prop test": "test val"}}"""; + var result = ScribanJsonTestHelper.RenderObject(template, inputJson); + Assert.AreEqual("test val", result["pkey"]?.ToString()); + } + // ── Global set key ───────────────────────────────────────────────────────── // Config: { target: 'gval', source: '', globalSetId: 'mySet', globalKey: 'region' } @@ -111,7 +129,7 @@ public void Parity_GlobalSetKey_RendersCorrectly() { const string template = """ { - "gval": {{ __globals__?.mySet["region"] | json }}, + "gval": {{ ((__globals__ ?? {})["mySet"] ?? {})["region"] | json }}, } """; const string inputJson = """{"__globals__": {"mySet": {"region": "eu-west"}}}"""; @@ -119,6 +137,36 @@ public void Parity_GlobalSetKey_RendersCorrectly() Assert.AreEqual("eu-west", result["gval"]?.ToString()); } + [TestMethod] + public void Parity_GlobalSetKey_MissingGlobals_ReturnsNull() + { + const string template = """ + { + "gval": {{ ((__globals__ ?? {})["mySet"] ?? {})["region"] | json }}, + } + """; + var result = ScribanJsonTestHelper.RenderObject(template, "{}"); + Assert.AreEqual(JTokenType.Null, result["gval"]?.Type); + } + + // ── Global set id with characters invalid in a bare identifier ───────────── + // Config: { target: 'gval', source: '', globalSetId: 'global-value-name', globalKey: 'baseUrl' } + // Regression: the old `__globals__?.global-value-name["baseUrl"]` shape tokenized the + // hyphens as subtraction and threw "Object `name` is null" — this is the exact bug report. + + [TestMethod] + public void Parity_GlobalSetKey_HyphenatedSetId_RendersCorrectly() + { + const string template = """ + { + "gval": {{ ((__globals__ ?? {})["global-value-name"] ?? {})["baseUrl"] | json }}, + } + """; + const string inputJson = """{"__globals__": {"global-value-name": {"baseUrl": "https://example.test"}}}"""; + var result = ScribanJsonTestHelper.RenderObject(template, inputJson); + Assert.AreEqual("https://example.test", result["gval"]?.ToString()); + } + // ── Lookup — null fallback ───────────────────────────────────────────────── // Config: { target: 'category', source: 'Cat', lookupDictionary: { entries:[{A→Alpha}], fallback:'null' } } diff --git a/SW.Bitween.UnitTests/SettingsProtectorTests.cs b/SW.Bitween.UnitTests/SettingsProtectorTests.cs new file mode 100644 index 00000000..98a501ef --- /dev/null +++ b/SW.Bitween.UnitTests/SettingsProtectorTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Security.Cryptography; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Services; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class SettingsProtectorTests +{ + /// + /// Shaped like a Rebex license key — the longest secret the catalog holds — but deliberately + /// not one. Nothing here depends on the contents, only on the round trip. + /// + private const string Secret = "==NOT-A-REAL-KEY-0123456789abcdefghijklmnop=="; + + private static SettingsProtector With(string passphrase) => + new(new BitweenOptions { SettingsEncryptionKey = passphrase }); + + [TestMethod] + public void Round_trips_a_secret() + { + var protector = With("a-passphrase"); + + Assert.AreEqual(Secret, protector.Unprotect(protector.Protect(Secret))); + } + + [TestMethod] + public void Stored_form_never_contains_the_plaintext() + { + var stored = With("a-passphrase").Protect(Secret); + + Assert.IsFalse(stored.Contains(Secret, StringComparison.Ordinal)); + // The version marker is what tells a stored value apart from a hand-written plain one. + Assert.IsTrue(stored.StartsWith("enc.v1:", StringComparison.Ordinal)); + } + + /// + /// Fresh salt and nonce per value, so two instances holding the same license key don't reveal + /// that by having identical rows. + /// + [TestMethod] + public void Encrypting_the_same_value_twice_gives_different_ciphertext() + { + var protector = With("a-passphrase"); + + Assert.AreNotEqual(protector.Protect(Secret), protector.Protect(Secret)); + } + + // Both of these surface as AuthenticationTagMismatchException (a CryptographicException): + // GCM can't distinguish "wrong key" from "altered bytes", and either way it refuses to + // hand back a plaintext rather than returning garbage. + [TestMethod] + public void A_different_passphrase_cannot_read_it() + { + var stored = With("a-passphrase").Protect(Secret); + + Assert.ThrowsException( + () => With("another-passphrase").Unprotect(stored)); + } + + [TestMethod] + public void Tampering_is_detected_rather_than_decrypted() + { + var protector = With("a-passphrase"); + var stored = protector.Protect(Secret); + // Flip the last ciphertext character — the authentication tag must reject it. + var tampered = stored[..^2] + (stored[^2] == 'A' ? 'B' : 'A') + stored[^1]; + + Assert.ThrowsException(() => protector.Unprotect(tampered)); + } + + /// A value written before a passphrase existed, or edited by hand, still reads back. + [TestMethod] + public void Unmarked_values_pass_through_untouched() + { + Assert.AreEqual("plain-text-value", With("a-passphrase").Unprotect("plain-text-value")); + Assert.AreEqual("", With("a-passphrase").Unprotect("")); + Assert.IsNull(With("a-passphrase").Unprotect(null)); + } + + [TestMethod] + public void Empty_secrets_are_stored_as_empty_not_as_ciphertext() + { + Assert.AreEqual("", With("a-passphrase").Protect("")); + } + + [TestMethod] + public void Without_a_passphrase_nothing_can_be_protected() + { + Assert.IsFalse(With(null).IsConfigured); + Assert.IsFalse(With(" ").IsConfigured); + Assert.ThrowsException(() => With(null).Protect(Secret)); + } +} diff --git a/SW.Bitween.Web/ClientApp/.gitignore b/SW.Bitween.Web/ClientApp/.gitignore new file mode 100644 index 00000000..7324fd27 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Playwright +test-results +playwright-report +blob-report +playwright/.cache + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/SW.Bitween.Web/ClientApp/.oxlintrc.json b/SW.Bitween.Web/ClientApp/.oxlintrc.json new file mode 100644 index 00000000..6fa991da --- /dev/null +++ b/SW.Bitween.Web/ClientApp/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/SW.Bitween.Web/ClientApp/README.md b/SW.Bitween.Web/ClientApp/README.md new file mode 100644 index 00000000..c9a64aea --- /dev/null +++ b/SW.Bitween.Web/ClientApp/README.md @@ -0,0 +1,60 @@ +# Bitween UI — redesign prototype + +Clean-slate redesign of the Bitween admin UI, hosted by `SW.Bitween.Web`. **Runs entirely on +mock data** — it never calls the API, even when served by it. Sub-phase 1 covers auth, dynamic +RBAC, user profile, and team management; other areas exist as permission-gated placeholder +pages so role gating can be demonstrated across the whole navigation. + +## Running it + +**UI-only (fastest loop):** + +```bash +npm install +npm run dev # → http://localhost:5173/ +``` + +**Backend-served (how it deploys):** + +```bash +npm run build # outputs into ../wwwroot with base / +dotnet run --project .. --launch-profile SW.Bitween.Web.Local +# → https://localhost:7155/ +``` + +`dotnet publish` runs the npm build automatically (skip with `-p:SkipClientBuild=true`); +the root `Dockerfile` builds the UI in its own node stage. The app lives under the host's +`/bitween` path base — runtime URLs must use `import.meta.env.BASE_URL` (see the project +instructions in `.github/instructions/`). + +Sign in with any prototype account listed on the login page (password `bitween`), or use the +one-click persona buttons. The floating **Demo** pill (bottom right) switches the signed-in +person at any time and resets the demo data. + +## Stack + +React 19 · TypeScript · Vite · Tailwind v4 · react-router v8 · TanStack Query · lucide-react. + +Brand carried over from the existing UI: the crimson ramp from `Bitween-UI/tailwind.config.js` +(verbatim, as `--color-crimson-*`) and the logo (`public/brand/`). Neutrals (`--color-ink-*`) +are derived from the logo's wordmark ink `#372f2e`. Tokens live in `src/index.css`. + +## Architecture — the parts that matter + +- **`src/api/` is the only data layer.** Components import `api` from `src/api/index.ts`, + which today points at `src/api/mock/mockClient.ts` (a localStorage-backed fake with + simulated latency). Implementing `ApiClient` (`src/api/client.ts`) over HTTP and changing + that one export swaps the whole app onto the real backend. +- **`src/api/permissions.ts` is the permission catalog** — every gated area and action. + Roles are named sets of these keys; a user's permissions are the union of their roles'. +- **`src/nav.ts` is the information architecture.** Sidebar, role-editor access preview and + post-login redirect all derive from it, each filtered by the session's permissions. +- **Gating hides, never dims**: `` for actions, `` for routes + (`src/auth/guards.tsx`). Unauthorized pages get an explanatory access-denied screen. +- **Everything is URL-addressable**: tabs are routes, the member drawer is a route, search + and filters are query params, dialogs are query params. +- Deployment assumptions (to revisit): served at root path, single project, client-side + routing compatible with being backend-served later. + +Anything the prototype needed that the real backend can't do yet is logged in +`Bitween-api/BACKEND_CAPABILITIES_NEEDED.md`. diff --git a/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts b/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts new file mode 100644 index 00000000..2cf8bfae --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from "@playwright/test"; + +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; + +test("dashboard loads with real aggregated data", async ({ page }) => { + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + + await page.goto("dashboard"); + await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("Exchanges today")).toBeVisible(); + await expect(page.getByText("Success rate (7 days)")).toBeVisible(); + await expect(page.getByText("undefined")).toHaveCount(0); + await expect(page.getByText("NaN")).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts b/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts new file mode 100644 index 00000000..7ba60137 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from "@playwright/test"; + +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; + +test.beforeEach(async ({ page }) => { + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); +}); + +test("exchanges list, filter, retry, bulk retry, create", async ({ page }) => { + test.setTimeout(45000); + await page.goto("exchanges"); + await expect(page.getByRole("row", { name: /azureBlob test sub/ }).first()).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("undefined")).toHaveCount(0); + // Avoid the background refetch racing with row selection below. + await page.getByLabel("Refresh interval").selectOption("0"); + + // Filter down to failed exchanges only. + await page.getByRole("button", { name: "Failed" }).click(); + const row = page.getByRole("row", { name: "36b3e2b1003048ff8dec1573b2f752c5" }); + await expect(row).toBeVisible({ timeout: 10000 }); + + // Expand the row (click the chevron cell — other cells stop propagation) + // and retry it from the drawer. + await row.locator("td").last().click(); + await page.getByRole("button", { name: "Retry…" }).click(); + await page.getByRole("dialog").getByRole("button", { name: "Retry" }).click(); + await expect(page.getByText(/Retry started/)).toBeVisible({ timeout: 10000 }); + + // Bulk retry a couple of specific rows (not the whole page — each retry does + // real file I/O against storage, so keep this fast and deterministic). + await page.getByRole("button", { name: "All" }).click(); + await expect(page.getByRole("checkbox", { name: "Select 18dfd10c4b764b53aea339eedb98de18" })).toBeVisible({ + timeout: 10000, + }); + await page.getByRole("checkbox", { name: "Select 18dfd10c4b764b53aea339eedb98de18" }).check(); + await page.getByRole("checkbox", { name: "Select a1b088fffe9e4993ac75736576a826cb" }).check(); + await expect(page.getByText(/\d+ selected/)).toBeVisible(); + await page.getByRole("button", { name: "Retry selected…" }).click(); + await page.getByRole("dialog").getByRole("button", { name: "Retry" }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0, { timeout: 15000 }); + + // Manually create an exchange addressed at an integration. + await page.goto("exchanges/new"); + await page.getByRole("combobox", { name: "Pick an integration…" }).click(); + await page.getByRole("option", { name: "s3 test sub" }).click(); + // Dismiss the dropdown panel via an outside click (it sits above the panel's + // anchor point, so it can't itself be covered) rather than Escape, which + // doesn't close this Headless UI combobox instance. + await page.getByRole("heading", { name: "New exchange" }).click(); + await expect(page.getByRole("listbox")).toHaveCount(0); + await page.locator("textarea").fill('{"test": true}'); + await page.getByRole("button", { name: "Create exchange" }).click(); + await expect(page).toHaveURL(/\/exchanges\?ids=/); + await expect(page.getByRole("row")).toHaveCount(2, { timeout: 10000 }); // header + the one new row +}); + +test("scheduled retries page loads", async ({ page }) => { + await page.goto("scheduled-retries"); + await expect(page.getByRole("heading", { name: "Scheduled retries" })).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("undefined")).toHaveCount(0); +}); + +test("queue health page loads with live consumer data", async ({ page }) => { + await page.goto("queue-health"); + await expect(page.getByRole("heading", { name: "Queue health" })).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("v3.local.bitween").first()).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("undefined")).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts b/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts new file mode 100644 index 00000000..da8358d6 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts @@ -0,0 +1,150 @@ +import { test, expect } from "@playwright/test"; + +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; + +test.beforeEach(async ({ page }) => { + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); +}); + +test("API gateway: create, attach partner, create integration detour, edit attachment, detach, delete", async ({ + page, +}) => { + const name = `Playwright API GW ${Date.now()}`; + + await page.goto("api-gateways/new"); + await page.fill("#nag-name", name); + await page.getByRole("button", { name: "Create gateway" }).click(); + await expect(page).toHaveURL(/\/api-gateways\/\d+$/); + await expect(page.getByRole("heading", { name })).toBeVisible(); + + // Attach a partner, detouring to create the required GatewayApiCall integration inline. + await page.getByRole("button", { name: "Attach partner" }).click(); + await expect(page).toHaveURL(/\/api-gateways\/\d+\/attach$/); + + await page.getByRole("button", { name: "acme" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + + const integrationName = `Playwright GW Integration ${Date.now()}`; + await page.getByRole("link", { name: "New integration" }).click(); + await expect(page).toHaveURL(/\/subscriptions\/new\?type=GatewayApiCall/); + await page.fill("#ni-name", integrationName); + await page.getByRole("button", { name: "test doc" }).click(); + await page.getByLabel("handler adapter").click(); + await page.getByRole("option", { name: "NativeHttpHandler" }).click(); + await expect(page.getByRole("listbox")).toHaveCount(0, { timeout: 10000 }); + await page.locator("#prop-Url").fill("https://example.com/sink"); + await page.getByRole("button", { name: "Create integration" }).click(); + + // This page itself renders a ReturnBanner with a "Continue" button before + // the mutation resolves (inherited from the detour link) — wait for the + // create to actually land on the new integration's own page first, or the + // click races and hits that stale button instead. + await expect(page).toHaveURL(/\/subscriptions\/\d+\?/); + await page.getByRole("button", { name: "Continue" }).click(); + await expect(page).toHaveURL(/\/api-gateways\/\d+\/attach$/); + await expect(page.getByText(integrationName)).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Attach partner" }).click(); + + await expect(page).toHaveURL(/\/api-gateways\/\d+$/); + await expect(page.getByText("acme").first()).toBeVisible(); + await expect(page.getByText(integrationName)).toBeVisible(); + + // Edit the attachment — exercises the remove-then-add path (backend's + // updatepartner can't mutate a composite-key column in place). + await page.getByRole("button", { name: "Edit attachment for acme" }).click(); + await expect(page).toHaveURL(/\/api-gateways\/\d+\/attachments\/\d+$/); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page).toHaveURL(/\/api-gateways\/\d+$/); + await expect(page.getByText(integrationName)).toBeVisible(); + + // Detach. + await page.getByRole("button", { name: "Detach acme" }).click(); + await page + .getByRole("dialog", { name: "Detach this partner?" }) + .getByRole("button", { name: "Detach partner" }) + .click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(page.getByText("No partners attached")).toBeVisible(); + + // Delete (no attachments left — exercises the plain path; cascade-delete + // path is covered separately since seed data already has an attached gateway). + await page.getByRole("button", { name: "Delete" }).click(); + await page + .getByRole("dialog", { name: "Delete this API gateway?" }) + .getByRole("button", { name: "Delete gateway" }) + .click(); + // ApiGatewayPage navigates to /api-gateways, which the router redirects to + // the unified integrations list. + await expect(page).toHaveURL(/\/subscriptions\?types=api-gateways$/); +}); + +test("Bus gateway: create, add route with match expression, edit route, remove, delete", async ({ page }) => { + const name = `Playwright Bus GW ${Date.now()}`; + + await page.goto("bus-gateways/new"); + await page.fill("#nbg-name", name); + await page.getByRole("button", { name: "test-hh" }).click(); + await page.getByRole("button", { name: "Create gateway" }).click(); + await expect(page).toHaveURL(/\/bus-gateways\/\d+$/); + await expect(page.getByRole("heading", { name })).toBeVisible(); + + await page.getByRole("button", { name: "Add route" }).click(); + await expect(page).toHaveURL(/\/bus-gateways\/\d+\/add-route$/); + + // Filter step — leave the match expression empty (null = matches everything). + await page.getByRole("button", { name: "Continue" }).click(); + // Partner step — no partner. + await page.getByRole("button", { name: "No partner" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + + // Integration step — detour to create the required BusGateway integration. + const integrationName = `Playwright Bus Integration ${Date.now()}`; + await page.getByRole("link", { name: "New integration" }).click(); + await expect(page).toHaveURL(/\/subscriptions\/new\?type=BusGateway/); + await page.fill("#ni-name", integrationName); + await page.getByLabel("handler adapter").click(); + await page.getByRole("option", { name: "NativeHttpHandler" }).click(); + await expect(page.getByRole("listbox")).toHaveCount(0, { timeout: 10000 }); + await page.locator("#prop-Url").fill("https://example.com/sink"); + await page.getByRole("button", { name: "Create integration" }).click(); + + // Wait for the create to actually land (see the comment in the API gateway + // test above) before clicking the ReturnBanner's "Continue". + await expect(page).toHaveURL(/\/subscriptions\/\d+\?/); + await page.getByRole("button", { name: "Continue" }).click(); + await expect(page).toHaveURL(/\/bus-gateways\/\d+\/add-route$/); + await expect(page.getByText(integrationName)).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Add route" }).click(); + + await expect(page).toHaveURL(/\/bus-gateways\/\d+$/); + await expect(page.getByText(integrationName)).toBeVisible(); + + // Edit the route (no-op save exercises the round trip of a null match expression). + await page.getByRole("button", { name: /Edit route \d+/ }).click(); + await expect(page).toHaveURL(/\/bus-gateways\/\d+\/routes\/\d+$/); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page).toHaveURL(/\/bus-gateways\/\d+$/); + + // Remove the route. + await page.getByRole("button", { name: /Remove route \d+/ }).click(); + await page + .getByRole("dialog", { name: "Remove this route?" }) + .getByRole("button", { name: "Remove route" }) + .click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(page.getByText("No routes")).toBeVisible(); + + await page.getByRole("button", { name: "Delete" }).click(); + await page + .getByRole("dialog", { name: "Delete this bus gateway?" }) + .getByRole("button", { name: "Delete gateway" }) + .click(); + await expect(page).toHaveURL(/\/subscriptions\?types=bus-gateways$/); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/global-setup.ts b/SW.Bitween.Web/ClientApp/e2e/global-setup.ts new file mode 100644 index 00000000..d89b59b5 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/global-setup.ts @@ -0,0 +1,85 @@ +import { request } from "@playwright/test"; + +/** + * Leaves the database in a known state before the suite runs. + * + * A test that fails part-way skips its own cleanup, and the leftovers aren't harmless: a stray + * account still holding Administrator is enough to make the last-administrator guard pass, which + * strips the seeded admin's role and fails everything after it. So rather than trusting each test + * to tidy up, every run starts by purging its own residue and putting the admin's role back. + */ + +const API = "https://localhost:7155/api"; +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; +/** Configured break-glass credentials — the only way in when the admin has no roles left. */ +const BREAK_GLASS = { username: "1", password: "1" }; +const ADMINISTRATOR_ROLE_ID = 1; + +const TEST_EMAIL = /^pw-.*@example\.test$/; +const TEST_ROLE = /^PW /; +/** The only settings the suite writes to — see the reset below for why this is a list, not "all". */ +const TEST_SETTINGS = ["Theme.PrimaryColor", "Theme.TabTitle", "Theme.CompanyName"]; + +interface Account { + id: number; + email: string; + roles: { id: number; name: string }[] | null; +} + +export default async function purgeTestData() { + const api = await request.newContext({ ignoreHTTPSErrors: true }); + + // Prefer the real admin; fall back to break-glass, which works even with no roles at all. + const login = await api.post(`${API}/accounts/login`, { + data: { Username: ADMIN_EMAIL, Password: ADMIN_PASSWORD }, + }); + let token: string = login.ok() ? (await login.json()).jwt : ""; + + const auth = () => ({ Authorization: `Bearer ${token}` }); + let accounts = await api.get(`${API}/accounts?limit=500`, { headers: auth() }); + + if (!accounts.ok()) { + const su = await api.post(`${API}/login`, { data: BREAK_GLASS }); + if (!su.ok()) + throw new Error( + "Can't reach the API as an administrator or via break-glass — is the local backend running?", + ); + token = (await su.json()).jwt; + accounts = await api.get(`${API}/accounts?limit=500`, { headers: auth() }); + } + + const rows: Account[] = (await accounts.json()).result ?? []; + + // The seeded admin must hold Administrator, or nothing downstream can manage anything. + const admin = rows.find((a) => a.email.toLowerCase() === ADMIN_EMAIL.toLowerCase()); + if (admin && !(admin.roles ?? []).some((r) => r.id === ADMINISTRATOR_ROLE_ID)) { + await api.post(`${API}/accounts/${admin.id}/setRoles`, { + headers: auth(), + data: { roleIds: [ADMINISTRATOR_ROLE_ID] }, + }); + // Re-read as the repaired admin so the purge below runs with full permissions. + const relogin = await api.post(`${API}/accounts/login`, { + data: { Username: ADMIN_EMAIL, Password: ADMIN_PASSWORD }, + }); + if (relogin.ok()) token = (await relogin.json()).jwt; + } + + for (const account of rows.filter((a) => TEST_EMAIL.test(a.email))) + await api.post(`${API}/accounts/${account.id}/remove`, { headers: auth(), data: {} }); + + const roles = await api.get(`${API}/roles?pageSize=500`, { headers: auth() }); + for (const role of ((await roles.json()).result ?? []) as { id: number; name: string }[]) + if (TEST_ROLE.test(role.name)) + await api.delete(`${API}/roles/${role.id}`, { headers: auth() }); + + // Settings tests assert against product defaults, so a value left behind by a failed run would + // make them fail for the wrong reason. Only the keys the tests actually touch are reset: the + // Settings table is now the only home for values like the MSAL ids and the Rebex license key — + // configuration is read once at first boot and ignored after that — so a blanket reset here + // would destroy real configuration with no way to get it back. + for (const key of TEST_SETTINGS) + await api.delete(`${API}/settings/${encodeURIComponent(key)}`, { headers: auth() }); + + await api.dispose(); +} diff --git a/SW.Bitween.Web/ClientApp/e2e/global-values.spec.ts b/SW.Bitween.Web/ClientApp/e2e/global-values.spec.ts new file mode 100644 index 00000000..91137fc6 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/global-values.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from "@playwright/test"; + +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; + +test.beforeEach(async ({ page }) => { + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); +}); + +test("global value set create, edit values, list, delete", async ({ page }) => { + const stamp = Date.now(); + const name = `Playwright Values ${stamp}`; + + await page.goto("global-values"); + await page.getByRole("button", { name: "New value set" }).click(); + await page.fill("#nvs-name", name); + await page.getByRole("button", { name: "Create value set" }).click(); + + await expect(page).toHaveURL(/\/global-values\/[a-z0-9-]+$/); + await expect(page.getByRole("heading", { name })).toBeVisible(); + + await page.getByRole("button", { name: "Add key" }).click(); + await page.getByLabel("Key 1").fill("baseUrl"); + await page.getByLabel("Value 1").fill("https://example.com"); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + + // Reload forces a fresh GET — proves the write actually persisted server-side. + await page.reload(); + await expect(page.getByLabel("Key 1")).toHaveValue("baseUrl"); + await expect(page.getByLabel("Value 1")).toHaveValue("https://example.com"); + + await page.goto("global-values"); + const row = page.getByRole("row", { name: new RegExp(name) }); + await expect(row).toBeVisible(); + await expect(row.locator("td").nth(2)).toHaveText("1"); // Values column + + await row.click(); + await page.getByRole("button", { name: "Delete" }).click(); + await page.getByRole("button", { name: "Delete value set" }).click(); + await expect(page).toHaveURL(/\/global-values$/); + await expect(page.getByText(name)).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/helpers.ts b/SW.Bitween.Web/ClientApp/e2e/helpers.ts new file mode 100644 index 00000000..cc9bccfa --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/helpers.ts @@ -0,0 +1,93 @@ +import type { Page } from "@playwright/test"; + +/** Checkbox labels carry their description in the accessible name, so anchor at the start. */ +export const startsWith = (text: string) => + new RegExp("^" + text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + +export const ADMIN_EMAIL = "admin@Bitween.systems"; +export const ADMIN_PASSWORD = "Mtm@dmin!2"; + +/** Passwords the members these tests create are given. Both clear the 8-character minimum. */ +export const FIRST_PASSWORD = "Pl4ywright!1"; +export const ROTATED_PASSWORD = "R0tated!Pass2"; + +export async function signIn(page: Page, email: string, password: string) { + await page.goto("login"); + await page.fill("#login-email", email); + await page.fill("#login-password", password); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); +} + +export const signInAsAdmin = (page: Page) => signIn(page, ADMIN_EMAIL, ADMIN_PASSWORD); + +export async function signOut(page: Page) { + // An open drawer lays a backdrop over the sidebar, which would swallow the click. + const drawer = page.getByRole("dialog", { name: "Member details" }); + if (await drawer.count()) { + await page.keyboard.press("Escape"); + await drawer.waitFor({ state: "detached" }); + } + await page.getByRole("button", { name: "Account menu" }).click(); + await page.getByRole("button", { name: "Sign out" }).click(); + await page.waitForURL((url) => url.pathname.endsWith("/login"), { timeout: 15000 }); +} + +/** + * Adds a member through the UI and returns their email. Unique per run so the suite can be + * re-run against the same database without colliding on the unique email index. + */ +export async function addMember( + page: Page, + { name, roles, password = FIRST_PASSWORD }: { name: string; roles: string[]; password?: string }, +) { + const email = `pw-${name.toLowerCase().replace(/\W+/g, "-")}-${Date.now()}@example.test`; + + await page.goto("team/members"); + await page.getByRole("button", { name: "Add member" }).click(); + await page.fill("#member-name", name); + await page.fill("#member-email", email); + await page.fill("#member-password", password); + for (const role of roles) await page.getByRole("checkbox", { name: startsWith(role) }).check(); + await page.getByRole("button", { name: "Add member" }).last().click(); + + await page.waitForSelector(`text=${email}`, { timeout: 15000 }); + return email; +} + +/** Creates a role holding exactly the given permissions. Returns its name. */ +export async function createRole( + page: Page, + { name, permissions }: { name: string; permissions: { area: string; action: string }[] }, +) { + await page.goto("team/roles/new"); + await page.fill("#role-name", name); + await page.fill("#role-desc", "Created by the Playwright suite."); + for (const { area, action } of permissions) + await page.getByRole("checkbox", { name: `${area}: ${action}`, exact: true }).check(); + await page.getByRole("button", { name: "Create role" }).click(); + await page.waitForURL(/\/team\/roles$/, { timeout: 15000 }); + return name; +} + +/** Opens a member's drawer from the members list. */ +export async function openMember(page: Page, email: string) { + await page.goto("team/members"); + await page.getByRole("row", { name: new RegExp(email) }).click(); + await page.getByRole("dialog", { name: "Member details" }).waitFor(); +} + +export async function removeMember(page: Page, email: string) { + await openMember(page, email); + await page.getByRole("button", { name: "Remove from team" }).click(); + await page.getByRole("button", { name: "Remove member" }).click(); + await page.getByRole("dialog", { name: "Member details" }).waitFor({ state: "detached" }); +} + +export async function deleteRole(page: Page, name: string) { + await page.goto("team/roles"); + await page.getByRole("link", { name: new RegExp(name) }).click(); + await page.getByRole("button", { name: "Delete role" }).click(); + await page.getByRole("button", { name: "Delete role" }).last().click(); + await page.waitForURL(/\/team\/roles$/, { timeout: 15000 }); +} diff --git a/SW.Bitween.Web/ClientApp/e2e/information-types.spec.ts b/SW.Bitween.Web/ClientApp/e2e/information-types.spec.ts new file mode 100644 index 00000000..25814e13 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/information-types.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from "@playwright/test"; + +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; + +test.beforeEach(async ({ page }) => { + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); +}); + +test("information type Code is optional end to end", async ({ page }) => { + const name = `Playwright No Code ${Date.now()}`; + + await page.goto("information-types/new"); + await page.fill("#nit-name", name); + + // Expand the collapsed code/format section and clear the auto-suggested code. + await page.getByRole("button", { name: /^Code/ }).click(); + const codeInput = page.locator("#nit-code"); + await expect(codeInput).not.toHaveAttribute("required", ""); + await codeInput.fill(""); + + await page.getByRole("button", { name: "Create information type" }).click(); + + // Should navigate straight to the detail page — no validation block on empty code. + await expect(page).toHaveURL(/\/information-types\/\d+$/); + await expect(page.getByRole("heading", { name })).toBeVisible(); + await expect(page.locator("#it-code")).toHaveValue(""); + // The identity badge next to the heading falls back to the name when there's no code. + await expect(page.getByRole("heading", { name }).locator("code")).toHaveText(name); + + // The list page's Code column falls back to the name too, not a blank/dash. + await page.goto("information-types"); + const row = page.getByRole("row", { name: new RegExp(name) }); + await expect(row.locator("code")).toHaveText(name); + + // Cleanup. + await row.click(); + await page.getByRole("button", { name: "Delete" }).click(); + await page.getByRole("button", { name: "Delete information type" }).click(); + await expect(page).toHaveURL(/\/information-types$/); + await expect(page.getByText(name)).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/integrations.spec.ts b/SW.Bitween.Web/ClientApp/e2e/integrations.spec.ts new file mode 100644 index 00000000..d680e71a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/integrations.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from "@playwright/test"; + +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; + +test.beforeEach(async ({ page }) => { + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); +}); + +test("scheduled job create, adapters, pause/resume, receive now, list, delete", async ({ page }) => { + const name = `Playwright Job ${Date.now()}`; + + await page.goto("scheduled-jobs/new"); + await page.fill("#sjw-name", name); + await page.getByRole("button", { name: "order" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + + // Source & schedule step — receiver adapter + its one required prop. + await page.getByLabel("receiver adapter").click(); + await page.getByRole("option", { name: "NativeHttpReceiver" }).click(); + await page.keyboard.press("Escape"); // close the combobox popover, it doesn't auto-dismiss + await expect(page.getByLabel("receiver adapter")).toHaveValue("NativeHttpReceiver"); + await page.locator("#prop-Url").fill("https://example.com/feed"); + await page.getByRole("button", { name: "Continue" }).click(); + + // Pipeline step — handler adapter + its required prop (mapper stays "None"). + await page.getByLabel("handler adapter").click(); + await page.getByRole("option", { name: "NativeHttpHandler" }).click(); + await expect(page.getByLabel("handler adapter")).toHaveValue("NativeHttpHandler"); + await expect(page.getByRole("listbox")).toHaveCount(0, { timeout: 10000 }); + await page.locator("#prop-Url").fill("https://example.com/sink"); + await page.getByRole("button", { name: "Continue" }).click(); + + // Review step — "Enable immediately" is checked by default. + await page.getByRole("button", { name: "Create scheduled job" }).click(); + + await expect(page).toHaveURL(/\/subscriptions\/\d+$/); + await expect(page.getByRole("heading", { name })).toBeVisible(); + await expect(page.getByRole("button", { name: "Active" })).toBeVisible(); + + // Pause / resume. + await page.getByRole("button", { name: "Pause" }).click(); + await page.getByRole("dialog", { name: "Pause this integration?" }).getByRole("button", { name: "Pause" }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(page.getByText("Paused", { exact: true }).first()).toBeVisible(); + + await page.getByRole("button", { name: "Resume" }).click(); + await page.getByRole("dialog", { name: "Resume this integration?" }).getByRole("button", { name: "Resume" }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(page.getByText("Paused", { exact: true })).toHaveCount(0); + + // Receive now. + await page.getByRole("button", { name: "Receive now" }).click(); + await page.getByRole("dialog", { name: "Receive now?" }).getByRole("button", { name: "Receive now" }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(page.getByText("Next run")).toBeVisible(); + + // Reload to prove the adapter config truly persisted server-side. + await page.reload(); + await expect(page.getByLabel("receiver adapter")).toHaveValue("NativeHttpReceiver"); + await expect(page.locator("#prop-Url").first()).toHaveValue("https://example.com/feed"); + await expect(page.getByLabel("handler adapter")).toHaveValue("NativeHttpHandler"); + + await page.goto("subscriptions?types=scheduled-jobs"); + const row = page.getByRole("row", { name: new RegExp(name) }); + await expect(row).toBeVisible({ timeout: 15000 }); + await expect(row.getByText("undefined")).toHaveCount(0); + + await row.getByRole("button", { name: `Open ${name}` }).click(); + await expect(page).toHaveURL(/\/subscriptions\/\d+$/); + await page.getByRole("button", { name: "Delete" }).click(); + await page.getByRole("button", { name: "Delete integration" }).click(); + await expect(page).toHaveURL(/\/subscriptions$/); + await expect(page.getByText(name)).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/login.spec.ts b/SW.Bitween.Web/ClientApp/e2e/login.spec.ts new file mode 100644 index 00000000..4ce0863a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/login.spec.ts @@ -0,0 +1,48 @@ +import { test, expect, type Page } from "@playwright/test"; + +/** + * What the sign-in page offers is driven by the anonymous config endpoint. These tests rewrite + * that response rather than saving the real setting: `Bitween.DisableEmailPasswordLogin` lives in + * the database, and a test that flipped it on and then failed would leave this instance reachable + * only through Microsoft — which the local profile has no MSAL app for. That's a locked door with + * no key, so the flag is exercised at the boundary instead. + */ +async function withConfig(page: Page, overrides: Record) { + await page.route("**/api/settings/config", async (route) => { + const real = await route.fetch(); + const body = await real.json(); + await route.fulfill({ json: { ...body, ...overrides } }); + }); +} + +const passwordField = (page: Page) => page.locator("#login-password"); +const microsoftButton = (page: Page) => page.getByRole("button", { name: "Continue with Microsoft" }); + +test("by default the sign-in page asks for an email and password", async ({ page }) => { + await page.goto("login"); + + await expect(passwordField(page)).toBeVisible(); + await expect(page.getByRole("button", { name: "Sign in" })).toBeVisible(); +}); + +test("Microsoft-only hides the password form instead of letting it fail", async ({ page }) => { + await withConfig(page, { disableEmailPasswordLogin: true, msalClientId: "00000000-0000-0000-0000-000000000000" }); + await page.goto("login"); + + // The backend rejects email/password outright in this mode, so the form must not be offered. + await expect(microsoftButton(page)).toBeVisible(); + await expect(passwordField(page)).toHaveCount(0); + await expect(page.getByRole("button", { name: "Sign in" })).toHaveCount(0); + // With nothing above it, the divider has nothing to divide. + await expect(page.getByText("or", { exact: true })).toHaveCount(0); +}); + +test("Microsoft-only with no Microsoft app configured explains itself", async ({ page }) => { + await withConfig(page, { disableEmailPasswordLogin: true, msalClientId: null }); + await page.goto("login"); + + // Both doors are shut. Saying so beats an empty card that looks like a failed page load. + await expect(page.getByText(/Microsoft sign-in isn't configured/)).toBeVisible(); + await expect(passwordField(page)).toHaveCount(0); + await expect(microsoftButton(page)).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts b/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts new file mode 100644 index 00000000..b2e29b85 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts @@ -0,0 +1,186 @@ +import { test, expect, type Page } from "@playwright/test"; +import { + FIRST_PASSWORD, + addMember, + createRole, + deleteRole, + removeMember, + signIn, + signInAsAdmin, + signOut, +} from "./helpers"; + +/** + * What a role grants has to hold in three places at once: the pages offered in the sidebar, the + * page reached by typing its URL, and the API behind the button. A permission system that only + * hides UI isn't one, so every check here ends at the server. + */ + +const sidebarLinks = (page: Page) => + page.getByRole("navigation").getByRole("link").filter({ hasNotText: /^$/ }); + +/** Calls the API directly with the signed-in member's own token — no UI in the way. */ +async function apiStatus( + page: Page, + method: "GET" | "POST", + path: string, + body?: unknown, +): Promise { + const token = await page.evaluate(() => localStorage.getItem("access_token")); + const res = await page.request.fetch(`https://localhost:7155/api${path}`, { + method, + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + ...(body === undefined ? {} : { data: body }), + }); + return res.status(); +} + +test("a custom role grants exactly what was ticked, in the nav, by URL, and at the API", async ({ + page, +}) => { + const roleName = `PW Exchange Watcher ${Date.now()}`; + await signInAsAdmin(page); + await createRole(page, { + name: roleName, + permissions: [{ area: "Exchanges", action: "View" }], + }); + const email = await addMember(page, { name: "Watcher Only", roles: [roleName] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + // 1. The sidebar offers the one page they can see, and nothing else. + await expect(sidebarLinks(page).filter({ hasText: "Exchanges" })).toBeVisible(); + for (const hidden of ["Partners", "Integrations", "Work groups", "Team", "Settings"]) + await expect(sidebarLinks(page).filter({ hasText: hidden })).toHaveCount(0); + + // 2. Typing the URL of a page they lack doesn't get them in. + await page.goto("partners"); + await expect(page.getByText("You don't have access to this page")).toBeVisible(); + + await page.goto("team/members"); + await expect(page.getByText("You don't have access to this page")).toBeVisible(); + + // 3. The page they do have loads, and offers no write actions. + await page.goto("exchanges"); + await expect(page.getByText("You don't have access to this page")).toHaveCount(0); + await expect(page.getByRole("button", { name: /^Retry/ })).toHaveCount(0); + + // 4. And the API refuses the same things, so a crafted request gains nothing. + expect(await apiStatus(page, "POST", "/partners", { name: "sneaky" })).toBe(401); + expect(await apiStatus(page, "GET", "/accounts?limit=5")).toBe(401); + expect(await apiStatus(page, "POST", "/roles", { name: "x", description: "", permissions: [] })).toBe(401); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); + await deleteRole(page, roleName); +}); + +test("Viewer can read but not write", async ({ page }) => { + await signInAsAdmin(page); + const email = await addMember(page, { name: "Read Only", roles: ["Viewer"] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + // Reading the configuration pages is fine. + await page.goto("partners"); + await expect(page.getByText("You don't have access to this page")).toHaveCount(0); + await expect(page.getByRole("button", { name: "New partner" })).toHaveCount(0); + + await page.goto("retry-policies"); + await expect(page.getByText("You don't have access to this page")).toHaveCount(0); + await expect(page.getByRole("button", { name: /^New retry policy/ })).toHaveCount(0); + + // Administration is out of reach entirely. + await expect(sidebarLinks(page).filter({ hasText: "Team" })).toHaveCount(0); + await page.goto("settings"); + await expect(page.getByText("You don't have access to this page")).toBeVisible(); + + // Writes are refused at the source, not just hidden. + expect(await apiStatus(page, "POST", "/partners", { name: "nope" })).toBe(401); + expect(await apiStatus(page, "POST", "/retrypolicies", { name: "nope" })).toBe(401); + // Work groups had no guard of any kind until the handlers were given one. + expect(await apiStatus(page, "POST", "/workgroups", { name: "nope", busMessageName: "nope" })).toBe(401); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); + +test("Member can configure integrations but not manage the team", async ({ page }) => { + await signInAsAdmin(page); + const email = await addMember(page, { name: "Regular Member", roles: ["Member"] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + await page.goto("partners"); + await expect(page.getByRole("button", { name: "New partner" })).toBeVisible(); + + // The whole Administration group is absent for a Member. + await expect(sidebarLinks(page).filter({ hasText: "Team" })).toHaveCount(0); + await expect(sidebarLinks(page).filter({ hasText: "Settings" })).toHaveCount(0); + + expect(await apiStatus(page, "GET", "/accounts?limit=5")).toBe(401); + expect(await apiStatus(page, "POST", "/roles", { name: "x", description: "", permissions: [] })).toBe(401); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); + +test("editing a role changes what its members can do, without them signing in again", async ({ + page, +}) => { + const roleName = `PW Growing ${Date.now()}`; + await signInAsAdmin(page); + await createRole(page, { name: roleName, permissions: [{ area: "Exchanges", action: "View" }] }); + const email = await addMember(page, { name: "Gains Access", roles: [roleName] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await expect(sidebarLinks(page).filter({ hasText: "Partners" })).toHaveCount(0); + + // Grant Partners while they're signed in. Permissions are resolved per request from the + // database rather than baked into the token, so this must take effect without a new login. + const admin = await page.context().browser()!.newContext({ ignoreHTTPSErrors: true }); + const adminPage = await admin.newPage(); + await signInAsAdmin(adminPage); + await adminPage.goto("team/roles"); + await adminPage.getByRole("link", { name: new RegExp(roleName) }).click(); + await adminPage.getByRole("checkbox", { name: "Partners: View", exact: true }).check(); + await adminPage.getByRole("button", { name: "Save changes" }).click(); + await adminPage.waitForURL(/\/team\/roles$/); + + await page.reload(); + await expect(sidebarLinks(page).filter({ hasText: "Partners" })).toBeVisible(); + await page.goto("partners"); + await expect(page.getByText("You don't have access to this page")).toHaveCount(0); + + await admin.close(); + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); + await deleteRole(page, roleName); +}); + +test("a member with no roles at all sees nothing and can do nothing", async ({ page }) => { + await signInAsAdmin(page); + const email = await addMember(page, { name: "No Roles", roles: [] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + await expect(sidebarLinks(page)).toHaveCount(0); + for (const path of ["exchanges", "partners", "team/members", "settings"]) { + await page.goto(path); + await expect(page.getByText("You don't have access to this page")).toBeVisible(); + } + expect(await apiStatus(page, "POST", "/partners", { name: "nope" })).toBe(401); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/retry-policies.spec.ts b/SW.Bitween.Web/ClientApp/e2e/retry-policies.spec.ts new file mode 100644 index 00000000..76bcf1d9 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/retry-policies.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from "@playwright/test"; + +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; + +test.beforeEach(async ({ page }) => { + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); +}); + +test("retry policy create, add group with fixed delay, dry-run, list, delete", async ({ page }) => { + const name = `Playwright Policy ${Date.now()}`; + const groupName = "Timeouts"; + + await page.goto("retry-policies"); + await page.getByRole("button", { name: "New retry policy" }).click(); + await page.fill("#nrp-name", name); + await page.getByRole("button", { name: "Create policy" }).click(); + + await expect(page).toHaveURL(/\/retry-policies\/\d+$/); + await expect(page.getByRole("heading", { name })).toBeVisible(); + await expect(page.getByText("No groups yet")).toBeVisible(); + + await page.getByRole("button", { name: "Add group" }).click(); + const dialog = page.getByRole("dialog", { name: "New group" }); + await dialog.locator("#rg-name").fill(groupName); + await dialog.getByRole("button", { name: "Add condition" }).click(); + await dialog.getByLabel("Text to find").fill("timeout"); + // Fixed delay in seconds — the backend stores this in milliseconds, so this + // exercises the ms <-> seconds conversion in both directions once reloaded. + await dialog.locator("#rg-delay").selectOption("fixed"); + await dialog.locator("#rg-d1").fill("45"); + await dialog.getByRole("button", { name: "Add group" }).click(); + + await expect(page.getByText(groupName)).toBeVisible(); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + + // Reload forces a fresh GET — proves the group (incl. the delay unit + // conversion) actually round-tripped through the backend correctly. + await page.reload(); + await expect(page.getByText(groupName)).toBeVisible(); + await page.getByRole("button", { name: `Edit ${groupName}` }).click(); + await expect(page.locator("#rg-delay")).toHaveValue("fixed"); + await expect(page.locator("#rg-d1")).toHaveValue("45"); + await page.getByRole("button", { name: "Cancel" }).click(); + + // Dry-run against the saved (now unsaved-clean) groups. + await page.fill("#tp-content", "System.Net.Http.HttpRequestException: timeout while connecting"); + await page.getByRole("button", { name: "Run simulation" }).click(); + const attempt = page.locator("ol li").first(); + await expect(attempt).toContainText("Retries"); + await expect(attempt).toContainText("Next try in 45s"); + + await page.goto("retry-policies"); + const row = page.getByRole("row", { name: new RegExp(name) }); + await expect(row).toBeVisible(); + await expect(row.locator("td").nth(1)).toHaveText("1"); // Groups column + + await row.click(); + await page.getByRole("button", { name: "Delete" }).click(); + await page.getByRole("button", { name: "Delete policy" }).click(); + await expect(page).toHaveURL(/\/retry-policies$/); + await expect(page.getByText(name)).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/settings.spec.ts b/SW.Bitween.Web/ClientApp/e2e/settings.spec.ts new file mode 100644 index 00000000..73b8461a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/settings.spec.ts @@ -0,0 +1,177 @@ +import { test, expect, type Page } from "@playwright/test"; +import { signInAsAdmin, signOut, startsWith } from "./helpers"; + +const TEAL = "#0f766e"; +/** A second colour, so the sign-in test stands on its own if the one above left residue. */ +const INDIGO = "#4338ca"; +const DEFAULT_COLOR = "#e3311d"; +const DEFAULT_CRON = "0 * * * * ?"; + +const brandColorVar = (page: Page) => + page.evaluate(() => document.documentElement.style.getPropertyValue("--color-crimson-600").trim()); + +/** The hex box beside the colour swatch; `exact` keeps it apart from the picker itself. */ +const hexInput = (page: Page) => page.getByRole("textbox", { name: "Primary color", exact: true }); + +async function openBrandSection(page: Page) { + await page.goto("settings"); + await page.getByRole("button", { name: "Brand & theme" }).click(); +} + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("sections come from the backend catalog, with no restart-required rows", async ({ page }) => { + await page.goto("settings"); + + for (const section of [ + "Documents & storage", + "API behavior", + "Single sign-on (Microsoft)", + "Adapters", + "Reliability & jobs", + "Messaging", + "Database", + "Security", + "Brand & theme", + ]) + await expect(page.getByRole("button", { name: section })).toBeVisible(); + + // Nothing carries a restart badge: a setting that couldn't take effect immediately is shown + // as an environment value instead of being offered as an edit that needs a restart to land. + await expect(page.getByText("Restart", { exact: true })).toHaveCount(0); +}); + +test("environment settings are shown but not offered as edits", async ({ page }) => { + await page.goto("settings"); + await page.getByRole("button", { name: "Database" }).click(); + + // A read-only row renders its value as text — there's no control carrying its label… + await expect(page.getByText("Use Azure managed identity")).toBeVisible(); + await expect(page.getByText("Off", { exact: true })).toBeVisible(); + await expect( + page.getByRole("textbox", { name: "Use Azure managed identity", exact: true }), + ).toHaveCount(0); + await expect(page.getByRole("checkbox")).toHaveCount(0); + + // …and a presence row reports only whether a value is set, never the value itself. + await expect(page.getByText("Not set", { exact: true })).toBeVisible(); + await expect( + page.getByRole("textbox", { name: "Managed identity client ID", exact: true }), + ).toHaveCount(0); + + // Neither kind can be reset, because neither is stored. + await expect(page.getByRole("button", { name: "Reset to default" })).toHaveCount(0); + await expect(page.getByText("Environment").first()).toBeVisible(); +}); + +test("Microsoft-only sign-in is an editable setting, not an environment value", async ({ page }) => { + await page.goto("settings"); + await page.getByRole("button", { name: "Single sign-on (Microsoft)" }).click(); + + // It applies per request — the Login handler and the config endpoint both read it live — so it + // belongs in the catalog as an edit rather than a read-only environment row. + const toggle = page.getByRole("checkbox", { name: startsWith("Off") }); + await expect(toggle).toBeVisible(); + await expect(toggle).not.toBeChecked(); + await expect(page.getByText("Microsoft sign-in only")).toBeVisible(); +}); + +test("the retry schedule is editable and rejects an invalid cron", async ({ page }) => { + await page.goto("settings"); + await page.getByRole("button", { name: "Reliability & jobs" }).click(); + + const cron = page.getByRole("textbox", { name: "Retry poll schedule", exact: true }); + await expect(cron).toHaveValue(DEFAULT_CRON); + + // The backend validates the expression before storing it, because a bad one would break the + // startup job seeding — so a rejected save leaves the draft dirty rather than silently passing. + await cron.fill("not a cron"); + await cron.blur(); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByText(/not a valid cron expression/)).toBeVisible(); + + await page.getByRole("button", { name: "Discard" }).click(); + await expect(cron).toHaveValue(DEFAULT_CRON); +}); + +test("brand colour: staged draft previews app-wide, saves, and resets", async ({ page }) => { + await openBrandSection(page); + const hex = hexInput(page); + await expect(hex).toHaveValue(DEFAULT_COLOR); + + await hex.fill(TEAL); + await hex.blur(); + await expect(page.getByText("Unsaved", { exact: true })).toBeVisible(); + // The draft previews immediately, before anything is saved. + expect(await brandColorVar(page)).toBe(TEAL); + + // ...and keeps previewing on other pages, with the banner offering a way back. Navigating + // in-app (rather than a hard load) is what a user does, and what lets the banner name the + // section holding the change. + await page.getByRole("link", { name: "Exchanges" }).click(); + await expect(page.getByText(/Previewing 1 unsaved setting change/)).toBeVisible(); + expect(await brandColorVar(page)).toBe(TEAL); + + await page.getByRole("button", { name: /Continue editing/ }).click(); + await expect(page).toHaveURL(/section=Brand/); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByText("Unsaved", { exact: true })).toHaveCount(0); + + // A reload proves it persisted server-side rather than living in the draft. + await page.reload(); + await expect(hexInput(page)).toHaveValue(TEAL); + expect(await brandColorVar(page)).toBe(TEAL); + + await page.getByRole("button", { name: "Reset to default" }).click(); + await page.getByRole("button", { name: "Save changes" }).click(); + await page.reload(); + await expect(hexInput(page)).toHaveValue(DEFAULT_COLOR); +}); + +test("a secret's value never reaches the browser", async ({ page }) => { + const payloads: string[] = []; + page.on("response", async (res) => { + if (res.url().endsWith("/api/settings")) payloads.push(await res.text()); + }); + + await page.goto("settings"); + await page.getByRole("button", { name: "Adapters" }).click(); + + // The local backend configures a Rebex key, so the row shows as set — masked, with the + // adapter-config "Replace" affordance rather than the value itself. + await expect(page.getByText("••••••••")).toBeVisible(); + await expect(page.getByRole("button", { name: "Replace" })).toBeVisible(); + + expect(payloads.length).toBeGreaterThan(0); + const rebex = JSON.parse(payloads[0]).find( + (r: { key: string }) => r.key === "Bitween.RebexLicenseKey", + ); + expect(rebex.secret).toBe(true); + expect(rebex.value).toBeNull(); + expect(rebex.defaultValue).toBe(""); + expect(rebex.hasValue).toBe(true); + // Editable because this instance has an encryption key configured; without one the row comes + // back read-only instead. + expect(rebex.editable).toBe(true); +}); + +test("the sign-in page brands itself before anyone has signed in", async ({ page }) => { + await openBrandSection(page); + await hexInput(page).fill(INDIGO); + await hexInput(page).blur(); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByText("Unsaved", { exact: true })).toHaveCount(0); + + await signOut(page); + // No session here at all — the login page reads branding from the anonymous config endpoint. + await expect.poll(() => brandColorVar(page)).toBe(INDIGO); + + await signInAsAdmin(page); + await openBrandSection(page); + await page.getByRole("button", { name: "Reset to default" }).click(); + await page.getByRole("button", { name: "Save changes" }).click(); + await page.reload(); + await expect(hexInput(page)).toHaveValue(DEFAULT_COLOR); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/team-members.spec.ts b/SW.Bitween.Web/ClientApp/e2e/team-members.spec.ts new file mode 100644 index 00000000..c3d93435 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/team-members.spec.ts @@ -0,0 +1,179 @@ +import { test, expect } from "@playwright/test"; +import { + ADMIN_EMAIL, + startsWith, + FIRST_PASSWORD, + ROTATED_PASSWORD, + addMember, + openMember, + removeMember, + signIn, + signInAsAdmin, + signOut, +} from "./helpers"; + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("add a member, they can sign in, then remove them", async ({ page }) => { + const email = await addMember(page, { name: "New Joiner", roles: ["Viewer"] }); + + const row = page.getByRole("row", { name: new RegExp(email) }); + await expect(row).toBeVisible(); + await expect(row).toContainText("Viewer"); + await expect(row).toContainText("Active"); + + // The account is real from the first moment — no accept step in between. + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await expect(page.getByRole("button", { name: "Account menu" })).toContainText("New Joiner"); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); + await expect(page.getByText(email)).toHaveCount(0); +}); + +test("change which roles a member holds", async ({ page }) => { + const email = await addMember(page, { name: "Role Swap", roles: ["Viewer"] }); + + await openMember(page, email); + const drawer = page.getByRole("dialog", { name: "Member details" }); + await drawer.getByRole("checkbox", { name: startsWith("Viewer") }).uncheck(); + await drawer.getByRole("checkbox", { name: startsWith("Member") }).check(); + await drawer.getByRole("button", { name: "Save roles" }).click(); + await expect(drawer.getByRole("button", { name: "Save roles" })).toHaveCount(0); + + // Reload rather than trust the optimistic UI — proves the write reached the database. + await page.reload(); + const row = page.getByRole("row", { name: new RegExp(email) }); + await expect(row).toContainText("Member"); + await expect(row).not.toContainText("Viewer"); + + await removeMember(page, email); +}); + +test("an administrator resets a member's password", async ({ page }) => { + const email = await addMember(page, { name: "Forgot Pass", roles: ["Viewer"] }); + + await openMember(page, email); + const drawer = page.getByRole("dialog", { name: "Member details" }); + await drawer.getByLabel("New password").fill(ROTATED_PASSWORD); + await drawer.getByRole("button", { name: "Set password" }).click(); + await expect(drawer.getByLabel("New password")).toHaveValue(""); + + await signOut(page); + await signIn(page, email, ROTATED_PASSWORD); + await expect(page.getByRole("button", { name: "Account menu" })).toContainText("Forgot Pass"); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); + +test("the old password stops working after a reset", async ({ page }) => { + const email = await addMember(page, { name: "Stale Pass", roles: ["Viewer"] }); + + await openMember(page, email); + const drawer = page.getByRole("dialog", { name: "Member details" }); + await drawer.getByLabel("New password").fill(ROTATED_PASSWORD); + await drawer.getByRole("button", { name: "Set password" }).click(); + await expect(drawer.getByLabel("New password")).toHaveValue(""); + + await signOut(page); + await page.fill("#login-email", email); + await page.fill("#login-password", FIRST_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await expect(page).toHaveURL(/\/login$/); + + await signInAsAdmin(page); + await removeMember(page, email); +}); + +test("disable a member, then re-enable them", async ({ page }) => { + const email = await addMember(page, { name: "On Leave", roles: ["Viewer"] }); + + await openMember(page, email); + const drawer = page.getByRole("dialog", { name: "Member details" }); + await drawer.getByRole("button", { name: "Disable account" }).click(); + await expect(drawer.getByRole("button", { name: "Re-enable account" })).toBeVisible(); + + await page.reload(); + await expect(page.getByRole("row", { name: new RegExp(email) })).toContainText("Disabled"); + + // A disabled account keeps its roles and history but must not be able to sign in. + await signOut(page); + await page.fill("#login-email", email); + await page.fill("#login-password", FIRST_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await expect(page).toHaveURL(/\/login$/); + + await signInAsAdmin(page); + await openMember(page, email); + await drawer.getByRole("button", { name: "Re-enable account" }).click(); + await expect(drawer.getByRole("button", { name: "Disable account" })).toBeVisible(); + + await removeMember(page, email); +}); + +test("the last administrator can't be removed or disabled", async ({ page }) => { + // This test only means anything while the seeded admin is the *only* administrator, and it's + // the one test that could strip its own role if the guard didn't fire. So assert the + // precondition rather than assume it, and put the role back if the save somehow goes through. + await page.goto("team/members"); + const admins = page.getByRole("row", { name: /Administrator/ }); + await expect( + admins, + "another account holds Administrator — the guard under test can't fire", + ).toHaveCount(1); + + await openMember(page, ADMIN_EMAIL); + const drawer = page.getByRole("dialog", { name: "Member details" }); + const role = drawer.getByRole("checkbox", { name: startsWith("Administrator") }); + + // Nothing destructive is even offered on your own account. + await expect(drawer.getByRole("button", { name: "Remove from team" })).toHaveCount(0); + await expect(drawer.getByRole("button", { name: "Disable account" })).toHaveCount(0); + + // Dropping the role is offered, but the server refuses it. + await role.uncheck(); + await drawer.getByRole("button", { name: "Save roles" }).click(); + + try { + await expect(drawer.getByText(/only member with the Administrator role/i)).toBeVisible(); + await page.reload(); + await expect(page.getByRole("row", { name: new RegExp(ADMIN_EMAIL) })).toContainText( + "Administrator", + ); + } finally { + // Belt and braces: if the guard let it through, put the role back before failing, so the + // rest of the suite doesn't run against an instance nobody can administer. Read the list + // fresh — the unchecked box in the drawer is a rejected draft, not what the server holds. + await page.goto("team/members"); + const adminRow = page.getByRole("row", { name: new RegExp(ADMIN_EMAIL) }); + if (!((await adminRow.textContent()) ?? "").includes("Administrator")) { + await adminRow.click(); + await drawer.getByRole("checkbox", { name: startsWith("Administrator") }).check(); + await drawer.getByRole("button", { name: "Save roles" }).click(); + await expect(drawer.getByRole("button", { name: "Save roles" })).toHaveCount(0); + } + } +}); + +test("filter and search the member list", async ({ page }) => { + const email = await addMember(page, { name: "Findable Person", roles: ["Viewer"] }); + + await page.getByLabel("Search members").fill("Findable"); + await expect(page.getByRole("row", { name: new RegExp(email) })).toBeVisible(); + await expect(page.getByRole("row", { name: new RegExp(ADMIN_EMAIL) })).toHaveCount(0); + + await page.getByLabel("Search members").fill(""); + await page.getByRole("button", { name: "Disabled", exact: true }).click(); + await expect(page.getByRole("row", { name: new RegExp(email) })).toHaveCount(0); + + await page.getByRole("button", { name: "Active", exact: true }).click(); + await expect(page.getByRole("row", { name: new RegExp(email) })).toBeVisible(); + + await removeMember(page, email); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/team-roles.spec.ts b/SW.Bitween.Web/ClientApp/e2e/team-roles.spec.ts new file mode 100644 index 00000000..75845a52 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/team-roles.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from "@playwright/test"; +import { addMember, createRole, deleteRole, removeMember, signInAsAdmin } from "./helpers"; + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("create a custom role, then delete it", async ({ page }) => { + const name = `PW Operator ${Date.now()}`; + + await createRole(page, { + name, + permissions: [ + { area: "Exchanges", action: "View" }, + { area: "Exchanges", action: "Operate" }, + ], + }); + + const row = page.getByRole("link", { name: new RegExp(name) }); + await expect(row).toBeVisible(); + await expect(row).toContainText("0 members"); + await expect(row).toContainText("2/"); + + // Reopen it: the permissions must come back from the server exactly as ticked. + await row.click(); + await expect(page.getByRole("checkbox", { name: "Exchanges: View", exact: true })).toBeChecked(); + await expect(page.getByRole("checkbox", { name: "Exchanges: Operate", exact: true })).toBeChecked(); + await expect( + page.getByRole("checkbox", { name: "Partners: View", exact: true }), + ).not.toBeChecked(); + + await page.goto("team/roles"); + await deleteRole(page, name); + await expect(page.getByText(name)).toHaveCount(0); +}); + +test("granting an action implies View, and clearing View clears the row", async ({ page }) => { + await page.goto("team/roles/new"); + + // An action you can't view is an action you can't reach, so View comes along. + const view = page.getByRole("checkbox", { name: "Partners: View", exact: true }); + const edit = page.getByRole("checkbox", { name: "Partners: Edit", exact: true }); + const del = page.getByRole("checkbox", { name: "Partners: Delete", exact: true }); + + // Only the count granted is asserted, not the catalog size — that changes whenever a + // permission is added or dropped, and it isn't what this test is about. + const granted = (n: number) => new RegExp(`\\b${n}/\\d+ permissions granted`); + + await edit.check(); + await expect(view).toBeChecked(); + await expect(page.getByText(granted(2))).toBeVisible(); + + await del.check(); + await expect(page.getByText(granted(3))).toBeVisible(); + + // Removing View takes the whole area with it. + await view.uncheck(); + await expect(edit).not.toBeChecked(); + await expect(del).not.toBeChecked(); + await expect(page.getByText(granted(0))).toBeVisible(); +}); + +test("the access preview shows what the role would see", async ({ page }) => { + await page.goto("team/roles/new"); + + await expect(page.getByText("No pages yet — grant a View permission.")).toBeVisible(); + + await page.getByRole("checkbox", { name: "Partners: View", exact: true }).check(); + const preview = page.locator("section, div").filter({ hasText: "What members with this role see" }).last(); + await expect(preview.getByText("Partners")).toBeVisible(); + await expect(preview.getByText("Exchanges")).toHaveCount(0); + + await page.getByRole("checkbox", { name: "Exchanges: View", exact: true }).check(); + await expect(preview.getByText("Exchanges")).toBeVisible(); +}); + +test("built-in roles are read-only", async ({ page }) => { + await page.goto("team/roles"); + await page.getByRole("link", { name: /Administrator/ }).click(); + + await expect(page.getByText("This role is built in")).toBeVisible(); + await expect(page.getByRole("checkbox", { name: "Partners: View", exact: true })).toBeDisabled(); + await expect(page.getByRole("button", { name: "Delete role" })).toHaveCount(0); + // Name and description aren't even rendered for a built-in. + await expect(page.locator("#role-name")).toHaveCount(0); +}); + +test("a role in use can't be deleted", async ({ page }) => { + const roleName = `PW InUse ${Date.now()}`; + await createRole(page, { name: roleName, permissions: [{ area: "Exchanges", action: "View" }] }); + const email = await addMember(page, { name: "Role Holder", roles: [roleName] }); + + await page.goto("team/roles"); + await expect(page.getByRole("link", { name: new RegExp(roleName) })).toContainText("1 member"); + + await page.getByRole("link", { name: new RegExp(roleName) }).click(); + await page.getByRole("button", { name: "Delete role" }).click(); + await page.getByRole("button", { name: "Delete role" }).last().click(); + await expect(page.getByText(/is still assigned to 1 member/i)).toBeVisible(); + + // Free the role up, and the delete goes through. + await removeMember(page, email); + await deleteRole(page, roleName); + await expect(page.getByText(roleName)).toHaveCount(0); +}); + +test("two roles can't share a name", async ({ page }) => { + await page.goto("team/roles/new"); + await page.fill("#role-name", "Administrator"); + await page.fill("#role-desc", "Should be refused."); + await page.getByRole("checkbox", { name: "Exchanges: View", exact: true }).check(); + await page.getByRole("button", { name: "Create role" }).click(); + + await expect(page.getByText(/already exists/i)).toBeVisible(); + await expect(page).toHaveURL(/\/team\/roles\/new$/); +}); + +test("duplicate a role", async ({ page }) => { + const original = `PW Source ${Date.now()}`; + await createRole(page, { + name: original, + permissions: [ + { area: "Partners", action: "View" }, + { area: "Partners", action: "Edit" }, + ], + }); + + await page.getByRole("link", { name: new RegExp(original) }).click(); + await page.getByRole("button", { name: "Duplicate" }).click(); + + await expect(page.locator("#role-name")).toHaveValue(`Copy of ${original}`); + await expect(page.getByRole("checkbox", { name: "Partners: Edit", exact: true })).toBeChecked(); + + const copy = `PW Copy ${Date.now()}`; + await page.fill("#role-name", copy); + await page.getByRole("button", { name: "Create role" }).click(); + await page.waitForURL(/\/team\/roles$/); + + await expect(page.getByRole("link", { name: new RegExp(copy) })).toBeVisible(); + + await deleteRole(page, copy); + await deleteRole(page, original); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/view-guards.spec.ts b/SW.Bitween.Web/ClientApp/e2e/view-guards.spec.ts new file mode 100644 index 00000000..be9133a7 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/view-guards.spec.ts @@ -0,0 +1,116 @@ +import { test, expect } from "@playwright/test"; +import { + FIRST_PASSWORD, + addMember, + createRole, + deleteRole, + removeMember, + signIn, + signInAsAdmin, + signOut, +} from "./helpers"; + +/** + * Reads are permission-guarded too, which is easy to get wrong in the other direction: a page can + * legitimately need data from an area the viewer has no business browsing. These cover both sides — + * what a narrow role can't read, and the pages it can still open in full. + */ + +async function apiStatus(page: import("@playwright/test").Page, path: string): Promise { + const token = await page.evaluate(() => localStorage.getItem("access_token")); + const res = await page.request.fetch(`https://localhost:7155/api${path}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + return res.status(); +} + +test("a role with one view permission can't read any other area's list", async ({ page }) => { + const roleName = `PW Docs Reader ${Date.now()}`; + await signInAsAdmin(page); + await createRole(page, { + name: roleName, + permissions: [{ area: "Information types", action: "View" }], + }); + const email = await addMember(page, { name: "Docs Reader", roles: [roleName] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + // The one area they hold is readable. + expect(await apiStatus(page, "/documents")).toBe(200); + + // Every other list is refused, not merely hidden in the nav. + for (const path of [ + "/partners", + "/xchanges", + "/subscriptions", + "/notifiers", + "/apigateways", + "/busgateways", + "/retrypolicies", + "/globaladaptervaluessets", + "/workgroups", + "/delayedretries", + "/ops/summary", + ]) + expect(await apiStatus(page, path), `${path} should be refused`).toBe(401); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); + await deleteRole(page, roleName); +}); + +test("lookup mode stays readable, because pickers across the app depend on it", async ({ page }) => { + const roleName = `PW Lookup Only ${Date.now()}`; + await signInAsAdmin(page); + await createRole(page, { + name: roleName, + permissions: [{ area: "Information types", action: "View" }], + }); + const email = await addMember(page, { name: "Lookup User", roles: [roleName] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + // id/name pairs only — what a picker needs, and not the data the guard protects. + for (const path of [ + "/partners?lookup=true", + "/subscriptions?lookup=true", + "/retrypolicies?lookup=true", + "/accounts?lookup=true", + ]) + expect([200, 206], `${path} should be allowed in lookup mode`).toContain( + await apiStatus(page, path), + ); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); + await deleteRole(page, roleName); +}); + +test("a page still loads when the area behind its Used by count is refused", async ({ page }) => { + const roleName = `PW No Integrations ${Date.now()}`; + await signInAsAdmin(page); + await createRole(page, { + name: roleName, + permissions: [{ area: "Information types", action: "View" }], + }); + const email = await addMember(page, { name: "No Integrations", roles: [roleName] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + // The information types list counts how many integrations use each type, which needs the + // integrations list this role can't read. The count is what's expendable, not the page. + await page.goto("information-types"); + await expect(page.getByText("You don't have access to this page")).toHaveCount(0); + await expect(page.getByRole("table")).toBeVisible(); + await expect(page.getByText(/failed|error/i)).toHaveCount(0); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); + await deleteRole(page, roleName); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/work-groups.spec.ts b/SW.Bitween.Web/ClientApp/e2e/work-groups.spec.ts new file mode 100644 index 00000000..793d714e --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/work-groups.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from "@playwright/test"; + +const ADMIN_EMAIL = "admin@Bitween.systems"; +const ADMIN_PASSWORD = "Mtm@dmin!2"; + +test.beforeEach(async ({ page }) => { + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); +}); + +test("work group create, edit queue settings, list, delete", async ({ page }) => { + const name = `Playwright Workgroup ${Date.now()}`; + + await page.goto("work-groups/new"); + await page.fill("#nwg-name", name); + await page.getByRole("button", { name: "Create work group" }).click(); + + await expect(page).toHaveURL(/\/work-groups\/\d+$/); + await expect(page.getByRole("heading", { name })).toBeVisible(); + // Defaults from the new-page form. + await expect(page.locator("#wg-prefetch")).toHaveValue("10"); + await expect(page.locator("#wg-priority")).toHaveValue("5"); + + await page.fill("#wg-prefetch", "25"); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + + // Reload forces a fresh GET — WorkGroups/Search.cs now reads the DB + // directly (not IInfolinkCache), so this reflects the update immediately. + await page.reload(); + await expect(page.locator("#wg-prefetch")).toHaveValue("25"); + + await page.goto("work-groups"); + const row = page.getByRole("row", { name: new RegExp(name) }); + await expect(row).toBeVisible(); + // A brand-new group has no consumers and nothing assigned to it, so both of + // those render as an em dash. + await expect(row.getByText("—")).toHaveCount(2); + + await row.getByRole("button", { name: `Open ${name}` }).click(); + await expect(page).toHaveURL(/\/work-groups\/\d+$/); + await page.getByRole("button", { name: "Delete" }).click(); + await page.getByRole("button", { name: "Delete work group" }).click(); + await expect(page).toHaveURL(/\/work-groups$/); + await expect(page.getByText(name)).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/index.html b/SW.Bitween.Web/ClientApp/index.html new file mode 100644 index 00000000..fb83c6a2 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/index.html @@ -0,0 +1,13 @@ + + + + + + + Bitween + + +
+ + + diff --git a/SW.Bitween.Web/ClientApp/package.json b/SW.Bitween.Web/ClientApp/package.json new file mode 100644 index 00000000..9d82fbce --- /dev/null +++ b/SW.Bitween.Web/ClientApp/package.json @@ -0,0 +1,40 @@ +{ + "name": "bitween-ui-next", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview", + "test": "vitest run", + "test:e2e": "playwright test" + }, + "dependencies": { + "@azure/msal-browser": "^4.0.1", + "@fontsource-variable/instrument-sans": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@headlessui/react": "^2.2.10", + "@monaco-editor/react": "^4.7.0", + "@tailwindcss/vite": "^4.3.2", + "@tanstack/react-query": "^5.101.2", + "immer": "^11.1.8", + "lucide-react": "^1.24.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router": "^8.2.0", + "tailwindcss": "^4.3.2" + }, + "devDependencies": { + "@playwright/test": "^1.61.1", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vitest": "^4.1.7" + } +} diff --git a/SW.Bitween.Web/ClientApp/playwright.config.ts b/SW.Bitween.Web/ClientApp/playwright.config.ts new file mode 100644 index 00000000..81c5ca1c --- /dev/null +++ b/SW.Bitween.Web/ClientApp/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "@playwright/test"; + +// Points at the locally running backend (SW.Bitween.Web.Local), which serves +// the built SPA at the site root — there is no separate dev server to boot. +export default defineConfig({ + testDir: "./e2e", + fullyParallel: false, + workers: 1, + reporter: "list", + // Tests run against a real database, so a failed run leaves data behind. This clears it and + // repairs the admin's roles before anything else, so one failure can't cascade into the next run. + globalSetup: "./e2e/global-setup.ts", + use: { + baseURL: "https://localhost:7155/", + ignoreHTTPSErrors: true, + }, +}); diff --git a/SW.Bitween.Web/ClientApp/public/brand/BitweenFull-light.svg b/SW.Bitween.Web/ClientApp/public/brand/BitweenFull-light.svg new file mode 100644 index 00000000..7fc64cc9 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/public/brand/BitweenFull-light.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SW.Bitween.Web/ClientApp/public/brand/BitweenFull.svg b/SW.Bitween.Web/ClientApp/public/brand/BitweenFull.svg new file mode 100644 index 00000000..1546a5c1 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/public/brand/BitweenFull.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SW.Bitween.Web/ClientApp/public/brand/BitweenIcon.png b/SW.Bitween.Web/ClientApp/public/brand/BitweenIcon.png new file mode 100644 index 00000000..1f22ce3f Binary files /dev/null and b/SW.Bitween.Web/ClientApp/public/brand/BitweenIcon.png differ diff --git a/SW.Bitween.Web/ClientApp/public/brand/BitweenIcon.svg b/SW.Bitween.Web/ClientApp/public/brand/BitweenIcon.svg new file mode 100644 index 00000000..b2e1faf3 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/public/brand/BitweenIcon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/SW.Bitween.Web/ClientApp/public/favicon.svg b/SW.Bitween.Web/ClientApp/public/favicon.svg new file mode 100644 index 00000000..6893eb13 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/SW.Bitween.Web/ClientApp/public/icons.svg b/SW.Bitween.Web/ClientApp/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SW.Bitween.Web/ClientApp/src/api/client.ts b/SW.Bitween.Web/ClientApp/src/api/client.ts new file mode 100644 index 00000000..c861fbae --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/client.ts @@ -0,0 +1,393 @@ +import type { AddBusRouteInput, AttachPartnerInput } from "./http/gateways"; +import type { + AdapterInfo, + AdapterKind, + ApiGateway, + ApiGatewayAttachment, + ApiGatewayDetail, + ApiGatewayRow, + BusGateway, + BusGatewayDetail, + BusGatewayRow, + DashboardData, + ExchangeQuery, + ExchangeRow, + GlobalValuesSetDetail, + GlobalValuesSetRow, + InformationType, + InformationTypeDetail, + InformationTypeRow, + Integration, + IntegrationDetail, + IntegrationInfo, + IntegrationLastRun, + IntegrationRow, + IntegrationRun, + IntegrationType, + MatchGroup, + Notifier, + NotifierDetail, + Paged, + Partner, + PartnerDetail, + PartnerRow, + PermissionArea, + PermissionKey, + QueueHealthSnapshot, + ReceiveAttemptRow, + ReceiveOutcome, + RetryGroup, + RetryAlertConfig, + RetryAttempts, + RetryPolicy, + RetryPolicyDetail, + RetryPolicyListRow, + RetryUsageRow, + RetryResultType, + RetryTestAttempt, + Role, + Schedule, + ScheduledRetryQuery, + ScheduledRetryRow, + ScheduleHealth, + Session, + SettingRow, + User, + WorkGroup, + WorkGroupDetail, + WorkGroupRow, +} from "./types"; + +/** + * The single data-access contract the UI is written against, implemented by + * ./http/httpClient. Components depend on this interface rather than on fetch, + * so an endpoint's shape can change in one place. + */ +export interface ApiClient { + // — session — + getSession(): Promise; + login(email: string, password: string): Promise; + loginWithMicrosoft(): Promise; + logout(): Promise; + + // — self service — + // No password-reset flow exists on the backend yet (BACKEND_WIRING_PLAN.md G3) — it needs + // outbound mail, which Bitween doesn't have. Hidden in the UI rather than faked. + updateProfile(changes: { displayName: string }): Promise; + changePassword(currentPassword: string, newPassword: string): Promise; + + // — members — + listUsers(): Promise; + getUser(id: string): Promise; + /** + * Creates a member outright, password and all. Bitween has no outbound mail, so there's no + * invite link — whoever adds the member passes the first password on themselves. + */ + createUser(input: { + displayName: string; + email: string; + password: string; + roleIds: string[]; + }): Promise; + /** Stands in for a self-service reset, which would need mail Bitween doesn't have. */ + setUserPassword(id: string, password: string): Promise; + updateUserRoles(id: string, roleIds: string[]): Promise; + /** Clears a failed-sign-in lockout early, rather than waiting it out. */ + unlockUser(id: string): Promise; + setUserDisabled(id: string, disabled: boolean): Promise; + deleteUser(id: string): Promise; + + // — roles — + /** The catalog the backend enforces, so the role matrix can never offer a grant it ignores. */ + getPermissionCatalog(): Promise; + listRoles(): Promise; + getRole(id: string): Promise; + createRole(input: { name: string; description: string; permissions: PermissionKey[] }): Promise; + updateRole( + id: string, + input: { name: string; description: string; permissions: PermissionKey[] }, + ): Promise; + deleteRole(id: string): Promise; + + // — partners — + listPartners(): Promise; + searchPartners(query: { search: string; offset: number; limit: number }): Promise>; + getPartner(id: number): Promise; + /** Light fetch used by the mapper editor's test-partner selector. */ + getPartnerAdapterProperties(id: number): Promise>; + createPartner(input: { name: string; adapterProperties?: Record }): Promise; + updatePartner( + id: number, + changes: { name?: string; adapterProperties?: Record }, + ): Promise; + deletePartner(id: number): Promise; + /** Returns the full key exactly once; afterwards only a prefix is ever shown. */ + addPartnerCredential(id: number, name: string): Promise<{ key: string }>; + revokePartnerCredential(id: number, name: string): Promise; + + // — information types — + listInformationTypes(): Promise; + searchInformationTypes(query: { + search: string; + offset: number; + limit: number; + }): Promise>; + getInformationType(id: number): Promise; + /** Same payload as update: a new type arrives complete, promoted properties included. */ + createInformationType( + input: Omit, + ): Promise; + updateInformationType( + id: number, + changes: Omit, + ): Promise; + deleteInformationType(id: number): Promise; + + // — global values — + listValueSets(): Promise; + getValueSet(id: string): Promise; + createValueSet(input: { + id: string; + name: string; + values: Record; + }): Promise; + updateValueSet( + id: string, + changes: { name: string; values: Record }, + ): Promise; + deleteValueSet(id: string): Promise; + + // — integrations (light summaries; cache aggressively) — + listIntegrations(): Promise; + + // — integrations — + listIntegrationRows(): Promise; + searchIntegrationRows(query: { + search: string; + type: IntegrationType | null; + informationTypeId?: number | null; + partnerId?: number | null; + inactive?: boolean | null; + offset: number; + limit: number; + }): Promise>; + getIntegration(id: number): Promise; + /** One call, one transaction: the integration exists as asked for, or not at all. */ + createIntegration(input: { + type: IntegrationType; + name: string; + informationTypeId: number; + /** Required by the types that carry their own partner — Internal and ApiCall. */ + partnerId?: number | null; + receiverId?: string | null; + receiverProperties?: Record; + validatorId?: string | null; + validatorProperties?: Record; + mapperId?: string | null; + mapperProperties?: Record; + handlerId?: string | null; + handlerProperties?: Record; + schedules?: Schedule[]; + retryPolicyId?: number | null; + responseIntegrationId?: number | null; + responseMessageTypeName?: string | null; + enabled?: boolean; + }): Promise; + updateIntegration( + id: number, + changes: Partial< + Pick< + Integration, + | "name" + | "enabled" + | "workGroupId" + | "retryPolicyId" + | "receiverId" + | "receiverProperties" + | "validatorId" + | "validatorProperties" + | "mapperId" + | "mapperProperties" + | "handlerId" + | "handlerProperties" + | "matchExpression" + | "schedules" + | "responseIntegrationId" + | "responseMessageTypeName" + > + >, + ): Promise; + deleteIntegration(id: number): Promise; + /** Toggles paused: paused integrations accept work but hold it. */ + pauseIntegration(id: number): Promise; + receiveNow(id: number): Promise; + /** Run history for one scheduled integration, newest first. Empty for unscheduled types. */ + listIntegrationRuns(id: number, limit?: number): Promise; + searchReceiveAttempts( + subscriptionId: number, + query: { outcome: ReceiveOutcome | null; offset: number; limit: number }, + ): Promise>; + /** Newest run of every scheduled integration — one request for a whole list. */ + listLastRuns(): Promise; + /** Will these schedules actually fire? Asks the scheduler, not the integration record. */ + listScheduleHealth(): Promise; + listAdapters(kind: AdapterKind): Promise; + + // — work groups — + listWorkGroups(): Promise; + searchWorkGroups(query: { search: string; offset: number; limit: number }): Promise>; + getWorkGroup(id: number): Promise; + createWorkGroup(input: { + name: string; + busMessageName: string; + prefetch: number; + priority: number; + }): Promise; + updateWorkGroup( + id: number, + changes: { name: string; busMessageName: string; prefetch: number; priority: number }, + ): Promise; + deleteWorkGroup(id: number): Promise; + + // — API gateways — + listApiGateways(): Promise; + searchApiGateways(query: { search: string; offset: number; limit: number }): Promise>; + getApiGateway(id: number): Promise; + searchGatewayAttachments( + apiGatewayId: number, + query: { search: string; offset: number; limit: number }, + ): Promise>; + createApiGateway(input: { name: string; urlName: string }): Promise; + updateApiGateway( + id: number, + changes: { name: string; urlName: string; inactive: boolean }, + ): Promise; + deleteApiGateway(id: number): Promise; + /** The integration is either an existing id or defined inline; the endpoint commits both as one. */ + attachGatewayPartner(id: number, input: AttachPartnerInput): Promise; + updateGatewayAttachment(id: number, input: { partnerId: number; integrationId: number }): Promise; + removeGatewayAttachment(id: number, partnerId: number): Promise; + + // — bus gateways — + listBusGateways(): Promise; + searchBusGateways(query: { + search: string; + informationTypeId?: number | null; + inactive?: boolean | null; + offset: number; + limit: number; + }): Promise>; + getBusGateway(id: number): Promise; + createBusGateway(input: { name: string; informationTypeId: number }): Promise; + updateBusGateway(id: number, changes: { name: string; inactive: boolean }): Promise; + deleteBusGateway(id: number): Promise; + /** The integration is either an existing id or defined inline; the endpoint commits both as one. */ + addBusRoute(id: number, input: AddBusRouteInput): Promise; + updateBusRoute( + id: number, + routeId: number, + input: { integrationId: number; partnerId: number | null; matchExpression: MatchGroup | null }, + ): Promise; + removeBusRoute(id: number, routeId: number): Promise; + + // — retry policies — + listRetryPolicies(): Promise; + searchRetryPolicies(query: { + search: string; + offset: number; + limit: number; + }): Promise>; + getRetryPolicy(id: number): Promise; + createRetryPolicy(input: { name: string }): Promise; + updateRetryPolicy( + id: number, + changes: { + name: string; + groups: RetryGroup[]; + alertHandlerId: string | null; + alertHandlerProperties: Record; + }, + ): Promise; + deleteRetryPolicy(id: number): Promise; + /** Dry-runs draft groups against a simulated failure over N attempts. */ + testRetryPolicy(input: { + groups: RetryGroup[]; + resultType: RetryResultType; + content: string; + attempts: number; + }): Promise; + + /** Spent budget and alert routing for every integration-and-group pair under this policy. */ + getRetryUsage(policyId: number): Promise; + /** + * The same report for one integration, which is the only way to reach one whose policy is an + * inline `CustomRetryPolicy` — those carry no policy id for the policy-scoped report to address, + * yet still spend budget and can sit stopped with no counter anyone can see. + */ + getIntegrationRetryUsage(integrationId: number): Promise; + /** The failures one group caught for one integration — what its spent budget went on. */ + getRetryAttempts(policyId: number, pair: { integrationId: number; groupId: string }): Promise; + /** Hands a spent budget back so the group retries again. Omit a field to reset across it. */ + resetRetryUsage(policyId: number, pair?: { integrationId?: number; groupId?: string }): Promise; + /** Reset by integration, for the inline-policy case the policy-scoped reset cannot reach. */ + resetIntegrationRetryUsage(integrationId: number, groupId?: string): Promise; + /** Sets, changes or clears where one pair's alert goes — the most specific level. */ + saveRetryAlertOverride( + policyId: number, + input: { integrationId: number; groupId: string } & RetryAlertConfig, + ): Promise; + + // — settings — + listSettings(): Promise; + /** `value: null` resets the setting back to its default. */ + updateSetting(key: string, value: string | null): Promise; + + // — notifiers — + // No backend delete/test-send endpoint exists yet (BACKEND_WIRING_PLAN.md G8) — hidden in the UI. + // Channel choices come from listAdapters("handler") — same catalog as any other handler slot. + searchNotifiers(query: { search: string; offset: number; limit: number }): Promise>; + getNotifier(id: number): Promise; + createNotifier(input: { name: string }): Promise; + updateNotifier(id: number, changes: Omit): Promise; + deleteNotifier(id: number): Promise; + + // — exchanges — + searchExchanges(query: ExchangeQuery): Promise>; + /** Fetches a stage document's raw text content by its storage key (`ExchangeFileRef.key`). */ + getExchangeDocument(key: string): Promise; + /** + * Re-runs an exchange from its input file. `reset` re-resolves adapter + * properties from the integration's current configuration instead of the + * values captured when the exchange first ran. Fails with + * AUTO_RETRY_SCHEDULED when an auto-retry is already pending. + */ + retryExchange(id: string, opts: { reset: boolean }): Promise<{ id: string }>; + /** Retries many; exchanges with a pending auto-retry are skipped, not failed. */ + bulkRetryExchanges(ids: string[], opts: { reset: boolean }): Promise<{ retried: number; skipped: number }>; + /** Manually injects a payload, addressed at an integration or an information type. */ + createExchange(input: { + target: "integration" | "informationType"; + integrationId?: number; + informationTypeId?: number; + data: string; + }): Promise<{ id: string }>; + + // — scheduled retries — + searchScheduledRetries(query: ScheduledRetryQuery): Promise>; + /** Executes a pending auto-retry immediately instead of waiting for its slot. */ + runScheduledRetryNow(id: string): Promise; + + // — queue health — + getQueueHealth(): Promise; + + // — dashboard — + getDashboard(): Promise; + + // — mappers — + /** Executes a Scriban template against sample input, injecting the partner's adapter properties and global value sets exactly as the runtime mapper does. */ + previewMapping(input: { + scribanTemplate: string; + inputJson: string; + partnerId?: number | null; + }): Promise<{ outputJson: string | null; error: string | null }>; +} diff --git a/SW.Bitween.Web/ClientApp/src/api/http/adapters.ts b/SW.Bitween.Web/ClientApp/src/api/http/adapters.ts new file mode 100644 index 00000000..c6938a49 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/adapters.ts @@ -0,0 +1,53 @@ +import type { ApiClient } from "../client"; +import type { AdapterInfo, AdapterKind, AdapterProp } from "../types"; +import { get } from "./request"; + +interface RawVersionedAdapter { + key: string; + versions: string[] | null; +} +interface RawStartupValue { + optional: boolean; + default: string | null; + private: boolean; + description: string | null; +} + +// The backend's Prefix param takes the plural, lowercase form. +const KIND_PREFIX: Record = { + receiver: "receivers", + handler: "handlers", + mapper: "mappers", + validator: "validators", +}; + +async function fetchProps(id: string): Promise { + const values = await get>(`/adapters/${encodeURIComponent(id)}/GetStartupValues`); + return Object.entries(values ?? {}).map(([key, v]) => ({ + key, + optional: v.optional, + default: v.default ?? undefined, + secret: v.private, + description: v.description ?? undefined, + })); +} + +export const adapterMethods = { + async listAdapters(kind: AdapterKind): Promise { + const rows = await get(`/adapters/Versioned?prefix=${KIND_PREFIX[kind]}`); + return Promise.all( + (rows ?? []).map(async (r) => ({ + id: r.key, + kind, + // No backend source for a friendly display name — fall back to the raw id. + label: r.key, + native: r.key.toLowerCase().startsWith("native"), + versions: r.versions ?? [], + // Legacy (non-native) adapters can fail to report startup values (e.g. their + // serverless runtime isn't available locally) — don't let that blank out the + // whole catalog, including the native adapters that did resolve fine. + props: await fetchProps(r.key).catch(() => []), + })), + ); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/appConfig.ts b/SW.Bitween.Web/ClientApp/src/api/http/appConfig.ts new file mode 100644 index 00000000..65216886 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/appConfig.ts @@ -0,0 +1,37 @@ +import { API_BASE } from "./request"; + +/** The `[Unprotect]` GET /settings/config payload the login page needs pre-auth. */ +export interface AppConfig { + msalClientId?: string | null; + msalTenantId?: string | null; + msalRedirectUri?: string | null; + /** When true the backend rejects email/password sign-in outright, so don't offer the form. */ + disableEmailPasswordLogin?: boolean; + isRabbitMqManagementConfigured?: boolean; + /** Effective brand values (any stored override already applied), keyed like `ThemeOptions`. */ + theme?: Record; + /** The same keys as configured, before any override — lets us tell "set" from "untouched". */ + themeDefaults?: Record; +} + +let cached: Promise | null = null; + +/** + * Bootstrap config served before authentication (MSAL parameters, feature + * flags, branding). Fetched once and memoised. Never throws to callers — an + * unreachable backend just yields an empty config, so the login page degrades + * gracefully. + */ +export function getAppConfig(): Promise { + if (!cached) { + cached = fetch(`${API_BASE}/settings/config`, { credentials: "include" }) + .then((res) => (res.ok ? (res.json() as Promise) : {})) + .catch(() => ({})); + } + return cached; +} + +/** Drops the memoised copy, so the next read picks up a brand setting that was just saved. */ +export function resetAppConfig(): void { + cached = null; +} diff --git a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts new file mode 100644 index 00000000..7a7279c1 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts @@ -0,0 +1,132 @@ +import type { ApiClient } from "../client"; +import type { DashboardData, ExchangeStatus } from "../types"; +import { integrationMethods } from "./integrations"; +import { get } from "./request"; + +// ——— backend shapes (camelCase over the wire) ——— +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawXchangeForDashboard { + id: string; + subscriptionId: number | null; + documentName: string; + status: boolean | null; + responseBad: boolean | null; + exception: string | null; + startedOn: string; +} +interface RawAlert { + severity: "Info" | "Warning" | "Critical"; +} + +const isBad = (raw: Pick) => + raw.status === false || (raw.status === true && raw.responseBad === true); +const isProcessing = (raw: Pick) => raw.status === null; +const toStatus = (raw: Pick): ExchangeStatus => + raw.status === null ? "processing" : !raw.status ? "failed" : raw.responseBad ? "badResponse" : "success"; + +export const dashboardMethods = { + async getDashboard(): Promise { + const dayMs = 86_400_000; + const startOfTodayUtc = new Date(new Date().setUTCHours(0, 0, 0, 0)).getTime(); + // 14 calendar days including today. + const windowStart = startOfTodayUtc - 13 * dayMs; + + // One bulk fetch covers every stat that's derived from exchange rows + // (today/yesterday/successRate7d/trafficByDay/busiest/latestFailures) — + // far fewer round trips than counting each bucket with its own filtered + // request, and exact rather than approximated. Bounded to a generous page + // size for what's a modest-scale ops tool; a very high-volume deployment + // would need real pagination here. + const [xchangeRes, delayedRes, alertsRaw, integrationRows] = await Promise.all([ + get>( + `/xchanges?filter=${encodeURIComponent(`StartedOn:6:${new Date(windowStart).toISOString()}`)}&size=1000&sort=StartedOn:1`, + ), + get>("/delayedretries?size=1"), + get("/ops/alerts"), + integrationMethods.listIntegrationRows(), + ]); + + const rows = xchangeRes.result; + const startedAt = (r: RawXchangeForDashboard) => Date.parse(r.startedOn); + const integrationNameById = new Map(integrationRows.map((i) => [i.id, i.name])); + + const todayRows = rows.filter((r) => startedAt(r) >= startOfTodayUtc); + const yesterdayRows = rows.filter((r) => { + const t = startedAt(r); + return t >= startOfTodayUtc - dayMs && t < startOfTodayUtc; + }); + // The last 7 calendar days including today — the same boundary as the + // final 7 entries of trafficByDay below. + const sevenDaysAgo = startOfTodayUtc - 6 * dayMs; + const week = rows.filter((r) => startedAt(r) >= sevenDaysAgo); + const finished7d = week.filter((r) => !isProcessing(r)); + const successRate7d = + finished7d.length === 0 + ? 100 + : Math.round((finished7d.filter((r) => !isBad(r)).length / finished7d.length) * 100); + + const trafficByDay = Array.from({ length: 14 }, (_, i) => { + const dayStart = windowStart + i * dayMs; + const dayRows = rows.filter((r) => { + const t = startedAt(r); + return t >= dayStart && t < dayStart + dayMs; + }); + return { + date: new Date(dayStart).toISOString(), + success: dayRows.filter((r) => !isBad(r)).length, + failed: dayRows.filter(isBad).length, + }; + }); + + const byIntegration = new Map(); + for (const r of week) { + if (r.subscriptionId === null) continue; + const entry = byIntegration.get(r.subscriptionId) ?? { count: 0, failed: 0 }; + entry.count++; + if (isBad(r)) entry.failed++; + byIntegration.set(r.subscriptionId, entry); + } + const busiest = [...byIntegration.entries()] + .map(([id, v]) => ({ id, name: integrationNameById.get(id) ?? `#${id}`, ...v })) + .sort((a, b) => b.count - a.count) + .slice(0, 5); + + const latestFailures = rows + .filter(isBad) + .sort((a, b) => b.startedOn.localeCompare(a.startedOn)) + .slice(0, 6) + .map((r) => ({ + id: r.id, + status: toStatus(r), + integrationId: r.subscriptionId, + integrationName: r.subscriptionId !== null ? (integrationNameById.get(r.subscriptionId) ?? null) : null, + informationTypeCode: r.documentName, + on: r.startedOn, + exception: r.exception, + })); + + return { + today: { + total: todayRows.length, + failed: todayRows.filter(isBad).length, + processing: todayRows.filter(isProcessing).length, + }, + yesterdayTotal: yesterdayRows.length, + successRate7d, + pendingRetries: delayedRes.totalCount, + queueAlerts: alertsRaw.filter((a) => a.severity !== "Info").length, + trafficByDay, + busiest, + latestFailures, + attention: { + failingIntegrations: integrationRows + .filter((i) => i.consecutiveFailures > 0) + .map((i) => ({ id: i.id, name: i.name, consecutiveFailures: i.consecutiveFailures })), + pausedIntegrations: integrationRows.filter((i) => i.paused).map((i) => ({ id: i.id, name: i.name })), + }, + }; + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/documents.ts b/SW.Bitween.Web/ClientApp/src/api/http/documents.ts new file mode 100644 index 00000000..9e1652ee --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/documents.ts @@ -0,0 +1,194 @@ +import type { ApiClient } from "../client"; +import type { + InformationType, + InformationTypeDetail, + InformationTypeFormat, + InformationTypeRow, + IntegrationType, + Paged, + TrailEntry, +} from "../types"; +import { exchangeMethods } from "./exchanges"; +import { gatewayMethods } from "./gateways"; +import { get, getEnrichment, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; + +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawKeyAndValue { + key: string; + value: string; +} +interface RawDocument { + id: number; + code: string | null; + name: string; + documentFormat: InformationTypeFormat; + busEnabled: boolean; + busMessageTypeName: string | null; + duplicateInterval: number; + disregardsUnfilteredMessages: boolean; + promotedProperties: RawKeyAndValue[] | null; +} +interface RawSubscriptionRef { + id: number; + name: string; + type: number | string; +} +interface RawTrailEntry { + createdOn: string; + code: "Created" | "Updated"; + createdBy: string; +} + +const SUB_TYPE_BY_NUM: Record = { + 1: "Internal", + 2: "ApiCall", + 4: "Receiving", + 8: "Aggregation", + 16: "GatewayApiCall", + 32: "BusGateway", +}; +const INTEGRATION_TYPES: IntegrationType[] = [ + "Receiving", + "GatewayApiCall", + "BusGateway", + "Internal", + "ApiCall", + "Aggregation", +]; +/** Enums may arrive as the numeric value or the name in any case. */ +const toIntegrationType = (t: number | string): IntegrationType => { + if (typeof t === "number") return SUB_TYPE_BY_NUM[t] ?? "Internal"; + return INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; +}; + +async function fetchSubscriptionsByDocument(documentId: number): Promise { + const res = await get>( + `/subscriptions?filter=${encodeURIComponent(`DocumentId:1:${documentId}`)}`, + ); + return res.result ?? []; +} + +async function fetchTrail(documentId: number): Promise { + const [trail, accountNames] = await Promise.all([ + get>(`/documents/trail?documentId=${documentId}&limit=8`), + get>("/accounts?lookup=true"), + ]); + return (trail.result ?? []).map((t) => ({ + on: t.createdOn, + action: t.code, + by: accountNames[t.createdBy] ?? "System", + byUserId: accountNames[t.createdBy] ? t.createdBy : undefined, + })); +} + +const toInformationType = (d: RawDocument): InformationType => ({ + id: d.id, + code: d.code ?? undefined, + name: d.name, + format: d.documentFormat, + busEnabled: d.busEnabled, + busMessageTypeName: d.busMessageTypeName ?? undefined, + duplicateIntervalMinutes: d.duplicateInterval, + disregardsUnfilteredMessages: d.disregardsUnfilteredMessages, + promotedProperties: (d.promotedProperties ?? []).map((p) => ({ key: p.key, path: p.value })), + createdOn: "", +}); + +async function fetchDetail(id: number): Promise { + const [d, subs, busGateways, recentExchanges, trail] = await Promise.all([ + get(`/documents/${id}`), + fetchSubscriptionsByDocument(id), + gatewayMethods.listBusGateways(), + exchangeMethods.searchExchanges({ informationTypeId: id, offset: 0, limit: 8 }), + fetchTrail(id), + ]); + return { + ...toInformationType(d), + integrationSetups: subs.map((s) => ({ id: s.id, name: s.name, type: toIntegrationType(s.type) })), + busGateways: busGateways + .filter((g) => g.informationTypeId === id) + .map((g) => ({ gatewayId: g.id, gatewayName: g.name })), + trail, + recentExchanges: recentExchanges.result.map((x) => ({ + id: x.id, + partnerName: x.partnerName ?? undefined, + informationTypeCode: x.informationTypeCode, + status: x.status, + on: x.startedOn, + promotedProperties: x.promotedProperties, + })), + }; +} + +/** One wire shape for both create and update, so they cannot drift apart. */ +const documentBody = (t: Omit) => ({ + code: t.code?.trim() || undefined, + name: t.name, + documentFormat: t.format, + busEnabled: t.busEnabled, + busMessageTypeName: t.busEnabled ? t.busMessageTypeName : undefined, + duplicateInterval: t.duplicateIntervalMinutes, + disregardsUnfilteredMessages: t.disregardsUnfilteredMessages, + promotedProperties: t.promotedProperties.map((p) => ({ key: p.key, value: p.path })), +}); + +export const documentMethods = { + async listInformationTypes(): Promise { + const [res, subs] = await Promise.all([ + get>("/documents"), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByDocument = new Map(); + for (const s of subs.result ?? []) + countByDocument.set(s.documentId, (countByDocument.get(s.documentId) ?? 0) + 1); + return (res.result ?? []).map((d) => ({ ...toInformationType(d), usedByCount: countByDocument.get(d.id) ?? 0 })); + }, + + async searchInformationTypes(query: { + search: string; + offset: number; + limit: number; + }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const [res, subs] = await Promise.all([ + get>(`/documents?${qs}`), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByDocument = new Map(); + for (const s of subs.result ?? []) + countByDocument.set(s.documentId, (countByDocument.get(s.documentId) ?? 0) + 1); + return { + total: res.totalCount, + result: (res.result ?? []).map((d) => ({ ...toInformationType(d), usedByCount: countByDocument.get(d.id) ?? 0 })), + }; + }, + + getInformationType: fetchDetail, + + async createInformationType( + input: Omit, + ): Promise { + const id = await post("/documents", documentBody(input)); + return fetchDetail(id); + }, + + async updateInformationType( + id: number, + changes: Omit, + ): Promise { + await post(`/documents/${id}`, { id, ...documentBody(changes) }); + return fetchDetail(id); + }, + + async deleteInformationType(id: number): Promise { + await request(`/documents/${id}`, { method: "DELETE" }); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts b/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts new file mode 100644 index 00000000..643236f1 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts @@ -0,0 +1,241 @@ +import type { ApiClient } from "../client"; +import type { + ExchangeQuery, + ExchangeRow, + ExchangeStatus, + Paged, + ScheduledRetryQuery, + ScheduledRetryRow, +} from "../types"; +import { partnerMethods } from "./partners"; +import { get, post } from "./request"; + +// ——— backend shapes (camelCase over the wire) ——— +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawXchangeRow { + id: string; + subscriptionId: number | null; + subscriptionName: string | null; + documentId: number; + documentName: string; + mapperId: string | null; + status: boolean | null; + exception: string | null; + finishedOn: string | null; + startedOn: string; + inputFileName: string | null; + outputFileName: string | null; + responseFileName: string | null; + inputKey: string | null; + outputKey: string | null; + responseKey: string | null; + promotedProperties: Record | null; + retryFor: string | null; + aggregationXchangeId: string | null; + responseBad: boolean | null; + correlationId: string | null; + partnerId: number | null; + scheduledRetryOn: string | null; +} +interface RawDelayedRetryRow { + id: string; + on: string; + subscriptionId: number | null; + subscriptionName: string | null; + documentId: number; + documentName: string; + exception: string | null; + startedOn: string; + promotedProperties: Record | null; + retryPolicyId: number | null; + retryPolicyName: string | null; +} + +/** + * The backend has no real ExchangeStatus enum — it's derived from two + * nullable booleans. `status == null` while still running; once set, a bad + * *response* (the handler delivered but the receiver answered with an error) + * is distinct from an outright failure. + */ +export const deriveStatus = (raw: Pick): ExchangeStatus => + raw.status === null ? "processing" : !raw.status ? "failed" : raw.responseBad ? "badResponse" : "success"; + +const STATUS_FILTER: Record = { + processing: 0, + success: 1, + badResponse: 2, + failed: 3, +}; + +const toExchangeRow = (raw: RawXchangeRow, partnerNameById: Map): ExchangeRow => ({ + id: raw.id, + status: deriveStatus(raw), + integrationId: raw.subscriptionId, + integrationName: raw.subscriptionName, + informationTypeId: raw.documentId, + informationTypeCode: raw.documentName, + partnerId: raw.partnerId, + partnerName: raw.partnerId !== null ? (partnerNameById.get(raw.partnerId) ?? null) : null, + startedOn: raw.startedOn, + finishedOn: raw.finishedOn, + correlationId: raw.correlationId, + retryFor: raw.retryFor, + aggregationXchangeId: raw.aggregationXchangeId, + scheduledRetryOn: raw.scheduledRetryOn, + exception: raw.exception, + promotedProperties: raw.promotedProperties, + mapperSkipped: raw.mapperId === null, + // Search's projection never populates file sizes/hashes (always 0 at the + // source) — show the name, which is real, with a size of 0 rather than + // fabricating one. Existence is keyed off `*Key` (backend only emits one once + // the file actually has bytes), not the file name, since gateway/manually + // created exchanges have no name yet content still exists. + files: { + input: raw.inputKey ? { name: raw.inputFileName ?? "input", size: 0, key: raw.inputKey } : null, + mapped: raw.outputKey ? { name: raw.outputFileName ?? "mapped", size: 0, key: raw.outputKey } : null, + handled: raw.responseKey ? { name: raw.responseFileName ?? "handled", size: 0, key: raw.responseKey } : null, + }, +}); + +const toScheduledRetryRow = (raw: RawDelayedRetryRow): ScheduledRetryRow => ({ + id: raw.id, + on: raw.on, + integrationId: raw.subscriptionId, + integrationName: raw.subscriptionName, + informationTypeId: raw.documentId, + informationTypeCode: raw.documentName, + exception: raw.exception, + startedOn: raw.startedOn, + promotedProperties: raw.promotedProperties, + retryPolicyId: raw.retryPolicyId, + retryPolicyName: raw.retryPolicyName, +}); + +/** + * The backend's date-`Range` filter (rule 21) is unconditionally broken: its + * shared parser (`SearchyFilter.cs`, SW-PrimitiveTypes) does `DateTime.Parse` + * with no `DateTimeStyles`, which can only produce `Local` or `Unspecified` + * `Kind` — Npgsql then refuses to bind it to a `timestamptz` column and the + * whole search 500s, regardless of what the client sends. Two scalar + * comparisons (`GreaterThanOrEquals`/`LessThanOrEquals`, rules 6/8) go + * through a different code path and work correctly — use those instead. + */ +function buildExchangeQuery(query: ExchangeQuery): string { + const params = new URLSearchParams(); + if (query.status) params.append("filter", `StatusFilter:1:${STATUS_FILTER[query.status]}`); + if (query.integrationId !== undefined) params.append("filter", `SubscriptionId:1:${query.integrationId}`); + if (query.partnerId !== undefined) params.append("filter", `PartnerId:1:${query.partnerId}`); + if (query.informationTypeId !== undefined) params.append("filter", `DocumentId:1:${query.informationTypeId}`); + if (query.ids?.trim()) { + const ids = query.ids.split(/[\s,|]+/).filter(Boolean); + params.append("filter", `Id:4:text|${ids.join("|")}`); + } + if (query.correlationId?.trim()) params.append("filter", `CorrelationId:1:${query.correlationId.trim()}`); + // PromotedPropertiesRaw is stored as "key:value,key:value", so prefixing the key turns + // the same substring match into a scoped one — no schema or endpoint change needed. + // Typing "merchant:Acme" into the value box has therefore always worked; the picker + // just makes it something you can find. + const propertyValue = query.property?.trim() ?? ""; + const propertyTerm = query.propertyKey ? `${query.propertyKey}:${propertyValue}` : propertyValue; + if (propertyTerm) params.append("filter", `PromotedPropertiesRaw:4:${propertyTerm}`); + if (query.from) params.append("filter", `StartedOn:6:${query.from}`); + if (query.to) params.append("filter", `StartedOn:8:${query.to}`); + params.set("page", String(Math.floor(query.offset / query.limit))); + params.set("size", String(query.limit)); + return params.toString(); +} + +function buildScheduledRetryQuery(query: ScheduledRetryQuery): string { + const params = new URLSearchParams(); + if (query.integrationId !== undefined) params.append("filter", `SubscriptionId:1:${query.integrationId}`); + if (query.informationTypeId !== undefined) params.append("filter", `DocumentId:1:${query.informationTypeId}`); + if (query.exception?.trim()) params.append("filter", `Exception:4:${query.exception.trim()}`); + if (query.from) params.append("filter", `On:6:${query.from}`); + if (query.to) params.append("filter", `On:8:${query.to}`); + params.set("page", String(Math.floor(query.offset / query.limit))); + params.set("size", String(query.limit)); + return params.toString(); +} + +async function partnerNameMap(): Promise> { + const partners = await partnerMethods.listPartners(); + return new Map(partners.map((p) => [p.id, p.name])); +} + +export const exchangeMethods = { + async getExchangeDocument(key: string): Promise { + const res = await get<{ data: string; key: string }>(`/bitweendocs?documentKey=${encodeURIComponent(key)}`); + return res.data; + }, + + async searchExchanges(query: ExchangeQuery): Promise> { + const [res, partnerNameById] = await Promise.all([ + get>(`/xchanges?${buildExchangeQuery(query)}`), + partnerNameMap(), + ]); + return { result: res.result.map((r) => toExchangeRow(r, partnerNameById)), total: res.totalCount }; + }, + + async retryExchange(id: string, { reset }: { reset: boolean }): Promise<{ id: string }> { + await post(`/xchanges/${id}/retry`, { reason: "Manual retry", reset }); + // Retry.cs returns null — look up the retry it just created (the newest + // xchange with retryFor == id) for a real id to hand back to the caller. + const res = await get>( + `/xchanges?filter=${encodeURIComponent(`RetryFor:1:${id}`)}&sort=StartedOn:2&size=1`, + ); + return { id: res.result[0]?.id ?? id }; + }, + + async bulkRetryExchanges(ids: string[], { reset }: { reset: boolean }): Promise<{ retried: number; skipped: number }> { + // BulkRetry.cs silently skips ids that already have a scheduled auto-retry + // and returns null — mirror its exact skip rule ourselves beforehand so we + // can report real counts back to the caller. + const idFilter = `Id:4:text|${ids.join("|")}`; + const current = await get>( + `/xchanges?filter=${encodeURIComponent(idFilter)}&size=${ids.length}`, + ); + // The Id filter also matches retryFor/aggregationXchangeId — narrow back + // down to exactly the requested ids. + const byId = new Map(current.result.filter((r) => ids.includes(r.id)).map((r) => [r.id, r])); + const skipped = ids.filter((id) => byId.get(id)?.scheduledRetryOn != null).length; + await post("/xchanges/bulkretry", { ids, reason: "Bulk retry", reset }); + return { retried: ids.length - skipped, skipped }; + }, + + async createExchange(input: { + target: "integration" | "informationType"; + integrationId?: number; + informationTypeId?: number; + data: string; + }): Promise<{ id: string }> { + const filter = + input.target === "integration" + ? `SubscriptionId:1:${input.integrationId}` + : `DocumentId:1:${input.informationTypeId}`; + await post("/xchanges", { + option: input.target === "integration" ? "SubscriberId" : "DocumentId", + subscriberId: input.target === "integration" ? input.integrationId : null, + documentId: input.target === "informationType" ? input.informationTypeId : null, + data: input.data, + }); + // Create.cs returns null too — look up the exchange it just created. When + // addressed at an information type, every matching integration gets its + // own exchange; we can only link to one, so take the newest. + const res = await get>( + `/xchanges?filter=${encodeURIComponent(filter)}&sort=StartedOn:2&size=1`, + ); + return { id: res.result[0]?.id ?? "" }; + }, + + async searchScheduledRetries(query: ScheduledRetryQuery): Promise> { + const res = await get>(`/delayedretries?${buildScheduledRetryQuery(query)}`); + return { result: res.result.map(toScheduledRetryRow), total: res.totalCount }; + }, + + async runScheduledRetryNow(id: string): Promise { + await post(`/delayedretries/${id}/runnow`, {}); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts b/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts new file mode 100644 index 00000000..ef76bcdd --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts @@ -0,0 +1,313 @@ +import type { ApiClient } from "../client"; +import type { + ApiGateway, + ApiGatewayAttachment, + ApiGatewayDetail, + ApiGatewayRow, + BusGateway, + BusGatewayDetail, + BusGatewayRoute, + BusGatewayRow, + InlineIntegrationDraft, + MatchGroup, + Paged, +} from "../types"; +import { toMatchGroup, toRawMatchExpression, type RawMatchSpec } from "./matchExpression"; +import { inlineIntegrationBody } from "./subscriptionBody"; +import { get, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; + +// ——— backend shapes (camelCase over the wire) ——— +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawApiGatewayPartner { + partnerId: number; + subscriptionId: number; + partnerName: string; + subscriptionName: string; +} +interface RawApiGateway { + id: number; + name: string; + urlName: string; + partnersCount: number | null; + inactive: boolean | null; + // Search's list projection includes this too (backend change made alongside + // this batch) — but keep it optional since Create's bare POST response has none. + partners: RawApiGatewayPartner[] | null; +} +interface RawBusGatewayRoute { + id: number; + subscriptionId: number; + subscriptionName: string | null; + partnerId: number | null; + partnerName: string | null; + matchExpression: RawMatchSpec | null; +} +interface RawBusGateway { + id: number; + name: string; + documentId: number; + documentName: string | null; + routesCount: number | null; + inactive: boolean | null; + routes: RawBusGatewayRoute[] | null; +} + +const toApiGatewayAttachment = (p: RawApiGatewayPartner): ApiGatewayAttachment => ({ + partnerId: p.partnerId, + partnerName: p.partnerName, + integrationId: p.subscriptionId, + integrationName: p.subscriptionName, +}); + +const toApiGatewayRow = (raw: RawApiGateway): ApiGatewayRow => ({ + id: raw.id, + name: raw.name, + urlName: raw.urlName, + inactive: raw.inactive ?? false, + createdOn: "", + partnerCount: raw.partnersCount ?? raw.partners?.length ?? 0, + attachments: (raw.partners ?? []).map(toApiGatewayAttachment), +}); + +const toApiGatewayDetail = (raw: RawApiGateway): ApiGatewayDetail => ({ + id: raw.id, + name: raw.name, + urlName: raw.urlName, + inactive: raw.inactive ?? false, + createdOn: "", + attachments: (raw.partners ?? []).map(toApiGatewayAttachment), +}); + +const toBusGatewayRoute = (r: RawBusGatewayRoute): BusGatewayRoute => ({ + id: r.id, + integrationId: r.subscriptionId, + integrationName: r.subscriptionName ?? "", + partnerId: r.partnerId, + partnerName: r.partnerName, + matchExpression: toMatchGroup(r.matchExpression), +}); + +const toBusGatewayRow = (raw: RawBusGateway): BusGatewayRow => ({ + id: raw.id, + name: raw.name, + informationTypeId: raw.documentId, + inactive: raw.inactive ?? false, + createdOn: "", + informationTypeCode: raw.documentName ?? "UNKNOWN", + routeCount: raw.routesCount ?? raw.routes?.length ?? 0, + routes: (raw.routes ?? []).map(toBusGatewayRoute), +}); + +const toBusGatewayDetail = (raw: RawBusGateway): BusGatewayDetail => ({ + id: raw.id, + name: raw.name, + informationTypeId: raw.documentId, + inactive: raw.inactive ?? false, + createdOn: "", + informationTypeCode: raw.documentName ?? "UNKNOWN", + informationTypeName: raw.documentName ?? "Unknown", + routes: (raw.routes ?? []).map(toBusGatewayRoute), +}); + +/** The attachment always points at an integration that already exists — a new one is + * created on its own page first, not inline here (unlike a bus gateway route, which + * still creates one in the same transaction — see `AddBusRouteInput`). */ +export type AttachPartnerInput = { partnerId: number; integrationId: number }; + +/** + * A route points at an integration that already exists, or defines one. Exactly one, + * which the endpoint enforces — the union makes that unrepresentable rather than + * merely wrong. + */ +export type AddBusRouteInput = { + partnerId: number | null; + matchExpression: MatchGroup | null; +} & ({ integrationId: number } | { newIntegration: InlineIntegrationDraft }); + +export const gatewayMethods = { + // ——— API gateways ——— + + async listApiGateways(): Promise { + const res = await get>("/apigateways"); + return (res.result ?? []).map(toApiGatewayRow); + }, + + async searchApiGateways(query: { search: string; offset: number; limit: number }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const res = await get>(`/apigateways?${qs}`); + return { total: res.totalCount, result: (res.result ?? []).map(toApiGatewayRow) }; + }, + + async getApiGateway(id: number): Promise { + return toApiGatewayDetail(await get(`/apigateways/${id}`)); + }, + + /** Paged, searched view of one gateway's attachments, for the gateway page's own + * table — `getApiGateway` keeps returning the full list, still needed by the + * attach-partner picker's exclude list. */ + async searchGatewayAttachments( + apiGatewayId: number, + query: { search: string; offset: number; limit: number }, + ): Promise> { + const params = new URLSearchParams({ + apiGatewayId: String(apiGatewayId), + offset: String(query.offset), + limit: String(query.limit), + }); + if (query.search.trim()) params.set("search", query.search.trim()); + const res = await get>(`/apigateways/attachments?${params.toString()}`); + return { total: res.totalCount, result: (res.result ?? []).map(toApiGatewayAttachment) }; + }, + + async createApiGateway({ name, urlName }: { name: string; urlName: string }): Promise { + const id = await post("/apigateways", { name, urlName, inactive: false }); + return { id, name, urlName, inactive: false, createdOn: "" }; + }, + + async updateApiGateway( + id: number, + changes: { name: string; urlName: string; inactive: boolean }, + ): Promise { + // Update replaces the record, so every field it accepts has to be sent back — + // omitting `inactive` would quietly reactivate a paused gateway on a rename. + await post(`/apigateways/${id}`, { + name: changes.name, + urlName: changes.urlName, + inactive: changes.inactive, + }); + return { id, ...changes, createdOn: "" }; + }, + + async deleteApiGateway(id: number): Promise { + await request(`/apigateways/${id}`, { method: "DELETE" }); + }, + + async attachGatewayPartner(id: number, input: AttachPartnerInput): Promise { + await post(`/apigateways/${id}/addpartner`, { + partnerId: input.partnerId, + subscriptionId: input.integrationId, + }); + }, + + async updateGatewayAttachment(id: number, input: { partnerId: number; integrationId: number }): Promise { + // Not a plain POST to updatepartner: ApiGatewayPartner's PK is the composite + // (gatewayId, partnerId, subscriptionId), and the backend's UpdatePartner + // handler tries to mutate subscriptionId in place on a tracked entity — EF + // Core rejects changes to a key column. Remove-then-add sidesteps it. + await post(`/apigateways/${id}/removepartner`, { partnerId: input.partnerId }); + await post(`/apigateways/${id}/addpartner`, { partnerId: input.partnerId, subscriptionId: input.integrationId }); + }, + + async removeGatewayAttachment(id: number, partnerId: number): Promise { + await post(`/apigateways/${id}/removepartner`, { partnerId }); + }, + + // ——— bus gateways ——— + + async listBusGateways(): Promise { + const res = await get>("/busgateways"); + return (res.result ?? []).map(toBusGatewayRow); + }, + + async searchBusGateways(query: { + search: string; + informationTypeId?: number | null; + inactive?: boolean | null; + offset: number; + limit: number; + }): Promise> { + const qs = buildListQuery({ + filters: [ + ["Name", SEARCHY_RULE.contains, query.search.trim()], + ["DocumentId", SEARCHY_RULE.equalsTo, query.informationTypeId ?? ""], + ["Inactive", SEARCHY_RULE.equalsTo, query.inactive == null ? "" : String(query.inactive)], + ], + offset: query.offset, + limit: query.limit, + }); + const res = await get>(`/busgateways?${qs}`); + return { total: res.totalCount, result: (res.result ?? []).map(toBusGatewayRow) }; + }, + + async getBusGateway(id: number): Promise { + return toBusGatewayDetail(await get(`/busgateways/${id}`)); + }, + + async createBusGateway({ + name, + informationTypeId, + }: { + name: string; + informationTypeId: number; + }): Promise { + const id = await post("/busgateways", { + name, + documentId: informationTypeId, + inactive: false, + }); + return { id, name, informationTypeId, inactive: false, createdOn: "" }; + }, + + async updateBusGateway( + id: number, + changes: { name: string; inactive: boolean }, + ): Promise { + // The bound information type is fixed at creation — Update.cs silently + // ignores documentId — but the request DTO still requires a value, so + // fetch the current one to round-trip it rather than sending a bogus 0. + const current = await get(`/busgateways/${id}`); + await post(`/busgateways/${id}`, { + name: changes.name, + documentId: current.documentId, + inactive: changes.inactive, + }); + return { + id, + name: changes.name, + informationTypeId: current.documentId, + inactive: changes.inactive, + createdOn: "", + }; + }, + + async deleteBusGateway(id: number): Promise { + await request(`/busgateways/${id}`, { method: "DELETE" }); + }, + + async addBusRoute(id: number, input: AddBusRouteInput): Promise { + await post(`/busgateways/${id}/addroute`, { + // Exactly one of the two, which is what the endpoint enforces. An integration + // defined here is created in the same transaction as the route. + ...("newIntegration" in input + ? { newIntegration: inlineIntegrationBody(input.newIntegration) } + : { subscriptionId: input.integrationId }), + partnerId: input.partnerId, + matchExpression: toRawMatchExpression(input.matchExpression), + }); + }, + + async updateBusRoute( + id: number, + routeId: number, + input: { integrationId: number; partnerId: number | null; matchExpression: MatchGroup | null }, + ): Promise { + await post(`/busgateways/${id}/updateroute`, { + routeId, + subscriptionId: input.integrationId, + partnerId: input.partnerId, + matchExpression: toRawMatchExpression(input.matchExpression), + }); + }, + + async removeBusRoute(id: number, routeId: number): Promise { + await post(`/busgateways/${id}/removeroute`, { routeId }); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/globalValues.ts b/SW.Bitween.Web/ClientApp/src/api/http/globalValues.ts new file mode 100644 index 00000000..38ec544c --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/globalValues.ts @@ -0,0 +1,112 @@ +import type { ApiClient } from "../client"; +import type { GlobalValuesSet, GlobalValuesSetDetail, GlobalValuesSetRow, IntegrationType, ValueSetUsage } from "../types"; +import { referencesGlobal, scanReferenceTokens } from "./references"; +import { get, getEnrichment, post } from "./request"; + +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawValueSet { + id: string; + name: string; + values: Record | null; +} +interface RawKeyAndValue { + key: string; + value: string; +} +interface RawSubscriptionForUsage { + id: number; + name: string; + type: number | string; + mapperProperties: RawKeyAndValue[] | null; + handlerProperties: RawKeyAndValue[] | null; + receiverProperties: RawKeyAndValue[] | null; + validatorProperties: RawKeyAndValue[] | null; +} + +const SUB_TYPE_BY_NUM: Record = { + 1: "Internal", + 2: "ApiCall", + 4: "Receiving", + 8: "Aggregation", + 16: "GatewayApiCall", + 32: "BusGateway", +}; +const INTEGRATION_TYPES: IntegrationType[] = [ + "Receiving", + "GatewayApiCall", + "BusGateway", + "Internal", + "ApiCall", + "Aggregation", +]; +/** Enums may arrive as the numeric value or the name in any case. */ +const toIntegrationType = (t: number | string): IntegrationType => { + if (typeof t === "number") return SUB_TYPE_BY_NUM[t] ?? "Internal"; + return INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; +}; + +// GlobalAdapterValuesSet has no CreatedOn column on the backend. +const toRow = (r: RawValueSet): GlobalValuesSet => ({ + id: r.id, + name: r.name, + values: r.values ?? {}, + createdOn: "", +}); + +async function fetchAllSubscriptionsForUsage(): Promise { + const res = await getEnrichment>("/subscriptions", { result: [], totalCount: 0 }); + return res.result ?? []; +} + +function globalKeysReferencedBy(sub: RawSubscriptionForUsage, setId: string): string[] { + const { globals } = scanReferenceTokens( + [sub.mapperProperties, sub.handlerProperties, sub.receiverProperties, sub.validatorProperties].flatMap( + (props) => (props ?? []).map((p) => p.value), + ), + ); + return globals.find((g) => referencesGlobal({ globals: [g] }, setId))?.keys ?? []; +} + + +export const globalValuesMethods = { + // No usage scan here: the list page answers "used by" from the integrations + // cache it already holds, so a second full /subscriptions fetch would be waste. + async listValueSets(): Promise { + const res = await get>("/globaladaptervaluessets"); + return (res.result ?? []).map(toRow); + }, + + async getValueSet(id: string): Promise { + const [r, subs] = await Promise.all([ + get(`/globaladaptervaluessets/${id}`), + fetchAllSubscriptionsForUsage(), + ]); + const usedBy: ValueSetUsage[] = subs + .map((s) => ({ + integrationSetup: { id: s.id, name: s.name, type: toIntegrationType(s.type) }, + keys: globalKeysReferencedBy(s, id), + })) + .filter((u) => u.keys.length > 0); + return { ...toRow(r), usedBy }; + }, + + async createValueSet(input: { id: string; name: string; values: Record }): Promise { + await post("/globaladaptervaluessets", { id: input.id, name: input.name, values: input.values }); + return toRow(input); + }, + + async updateValueSet( + id: string, + changes: { name: string; values: Record }, + ): Promise { + await post(`/globaladaptervaluessets/${id}`, { name: changes.name, values: changes.values }); + return toRow({ id, ...changes }); + }, + + async deleteValueSet(id: string): Promise { + await post(`/globaladaptervaluessets/${id}/delete`, {}); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts b/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts new file mode 100644 index 00000000..9b3dd3e7 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts @@ -0,0 +1,51 @@ +import type { ApiClient } from "../client"; +import { NotWiredError } from "../types"; +import { adapterMethods } from "./adapters"; +import { dashboardMethods } from "./dashboard"; +import { documentMethods } from "./documents"; +import { exchangeMethods } from "./exchanges"; +import { gatewayMethods } from "./gateways"; +import { globalValuesMethods } from "./globalValues"; +import { integrationMethods } from "./integrations"; +import { mapperMethods } from "./mappers"; +import { notifierMethods } from "./notifiers"; +import { partnerMethods } from "./partners"; +import { queueHealthMethods } from "./queueHealth"; +import { retryPolicyMethods } from "./retryPolicies"; +import { sessionMethods } from "./session"; +import { settingsMethods } from "./settings"; +import { teamMethods } from "./team"; +import { workGroupMethods } from "./workGroups"; + +/** + * The single real client. Wired domains are merged in here; every other + * ApiClient method resolves to a rejected NotWiredError so its screen shows an + * honest "Not connected yet" state instead of fake data. Each batch adds its + * domain module to `wired` (see BACKEND_WIRING_PLAN §5–6). + */ +const wired: Partial = { + ...sessionMethods, + ...partnerMethods, + ...documentMethods, + ...globalValuesMethods, + ...workGroupMethods, + ...retryPolicyMethods, + ...integrationMethods, + ...adapterMethods, + ...gatewayMethods, + ...exchangeMethods, + ...queueHealthMethods, + ...dashboardMethods, + ...mapperMethods, + ...notifierMethods, + ...teamMethods, + ...settingsMethods, +}; + +export const httpClient: ApiClient = new Proxy(wired, { + get(target, prop, receiver) { + if (typeof prop !== "string" || prop in target) return Reflect.get(target, prop, receiver); + // Anything not yet wired: a callable that rejects clearly, so `await api.x()` fails honestly. + return () => Promise.reject(new NotWiredError(prop)); + }, +}) as ApiClient; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts b/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts new file mode 100644 index 00000000..910ce616 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts @@ -0,0 +1,520 @@ +import type { ApiClient } from "../client"; +import { + ApiRequestError, + type Integration, + type IntegrationDetail, + type IntegrationInfo, + type IntegrationLastRun, + type IntegrationRow, + type IntegrationRun, + type IntegrationType, + type InformationTypeRow, + type Paged, + type PartnerRow, + type ReceiveAttemptRow, + type ReceiveOutcome, + type Schedule, + type ScheduleHealth, +} from "../types"; +import { schedulesSummary } from "../../lib/schedules"; +import { documentMethods } from "./documents"; +import { deriveStatus, exchangeMethods } from "./exchanges"; +import { gatewayMethods } from "./gateways"; +import { partnerMethods } from "./partners"; +import { scanReferenceTokens } from "./references"; +import { get, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE, SEARCHY_SORT } from "./searchQuery"; +import { toMatchGroup, toRawMatchExpression, type RawMatchSpec } from "./matchExpression"; +import { + toKvArray, + toRawSchedules, + type RawKeyAndValue, + type RawSchedule, +} from "./subscriptionBody"; + +// ——— backend shapes (camelCase over the wire) ——— +interface SearchyResponse { + result: T[]; + totalCount: number; +} + +interface RawReceiveAttemptExchange { + id: string; + status: boolean | null; + responseBad: boolean | null; + promotedProperties: Record | null; +} +interface RawReceiveAttempt { + id: number; + startedOn: string; + finishedOn: string; + // Enums may arrive as the numeric value or the name, depending on the endpoint. + outcome: number | string; + errorMessage: string | null; + exchanges: RawReceiveAttemptExchange[]; +} +const RECEIVE_OUTCOME_BY_NUM: Record = { + 0: "Failed", + 1: "NoNewData", + 2: "Received", +}; +const toReceiveOutcome = (o: number | string): ReceiveOutcome => + typeof o === "number" ? (RECEIVE_OUTCOME_BY_NUM[o] ?? "Failed") : (o as ReceiveOutcome); + +interface RawSubscription { + id?: number; + name: string; + documentId: number; + partnerId: number | null; + aggregationForId: number | null; + type: string; + handlerId: string | null; + mapperId: string | null; + receiverId: string | null; + validatorId: string | null; + inactive: boolean; + temporary: boolean; + categoryId: number | null; + handlerProperties: RawKeyAndValue[] | null; + mapperProperties: RawKeyAndValue[] | null; + receiverProperties: RawKeyAndValue[] | null; + validatorProperties: RawKeyAndValue[] | null; + documentFilter: RawKeyAndValue[] | null; + matchExpression: RawMatchSpec | null; + workGroupId: number | null; + retryPolicyId: number | null; + customRetryPolicy: unknown | null; + schedules: RawSchedule[] | null; + responseSubscriptionId: number | null; + responseMessageTypeName: string | null; + receiveOn: string | null; + aggregateOn: string | null; + pausedOn: string | null; + isRunning: boolean | null; + consecutiveFailures: number; + lastException: string | null; + aggregationTarget?: string; +} + +const SUB_TYPE_BY_NUM: Record = { + 1: "Internal", + 2: "ApiCall", + 4: "Receiving", + 8: "Aggregation", + 16: "GatewayApiCall", + 32: "BusGateway", +}; +const INTEGRATION_TYPES: IntegrationType[] = [ + "Receiving", + "GatewayApiCall", + "BusGateway", + "Internal", + "ApiCall", + "Aggregation", +]; +/** Enums serialize as their C# member name string, but guard the numeric case too. */ +const toIntegrationType = (t: number | string): IntegrationType => + typeof t === "number" + ? (SUB_TYPE_BY_NUM[t] ?? "Internal") + : (INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"); + +// Empty-valued properties are dropped: an adapter property with no value means +// "not set", and keeping it would make a freshly-cleared field compare unequal to +// stored data that never had the key — leaving the Save bar up after an undo. +const toRecord = (kvs: RawKeyAndValue[] | null): Record => + Object.fromEntries((kvs ?? []).filter((kv) => kv.value !== "").map((kv) => [kv.key, kv.value])); + +const toSchedules = (raw: RawSchedule[] | null): Schedule[] => + (raw ?? []).map((s) => ({ + recurrence: s.recurrence, + days: s.days, + hours: s.hours, + minutes: s.minutes, + backwards: s.backwards, + })); + +function toIntegration(raw: RawSubscription, idOverride?: number): Integration { + return { + id: raw.id ?? idOverride!, + name: raw.name, + type: toIntegrationType(raw.type), + informationTypeId: raw.documentId, + partnerId: raw.partnerId, + enabled: !raw.inactive, + pausedOn: raw.pausedOn ?? null, + workGroupId: raw.workGroupId ?? null, + retryPolicyId: raw.retryPolicyId ?? null, + receiverId: raw.receiverId ?? null, + receiverProperties: toRecord(raw.receiverProperties), + validatorId: raw.validatorId ?? null, + validatorProperties: toRecord(raw.validatorProperties), + mapperId: raw.mapperId ?? null, + mapperProperties: toRecord(raw.mapperProperties), + handlerId: raw.handlerId ?? null, + handlerProperties: toRecord(raw.handlerProperties), + matchExpression: toMatchGroup(raw.matchExpression), + schedules: toSchedules(raw.schedules), + responseIntegrationId: raw.responseSubscriptionId ?? null, + responseMessageTypeName: raw.responseMessageTypeName ?? null, + aggregationForId: raw.aggregationForId ?? null, + isRunning: raw.isRunning ?? false, + nextReceiveOn: raw.receiveOn ?? null, + consecutiveFailures: raw.consecutiveFailures ?? 0, + lastException: raw.lastException ?? null, + // Subscription has no CreatedOn column on the backend. + createdOn: "", + }; +} + +async function fetchRaw(id: number): Promise { + const raw = await get(`/subscriptions/${id}`); + if (!raw) throw new ApiRequestError("NOT_FOUND", "This integration no longer exists."); + return raw; +} + +async function fetchAllRaw(): Promise { + const res = await get>("/subscriptions"); + return res.result ?? []; +} + +type UpdatableFields = Partial< + Pick< + Integration, + | "name" + | "enabled" + | "workGroupId" + | "retryPolicyId" + | "receiverId" + | "receiverProperties" + | "validatorId" + | "validatorProperties" + | "mapperId" + | "mapperProperties" + | "handlerId" + | "handlerProperties" + | "matchExpression" + | "schedules" + | "responseIntegrationId" + | "responseMessageTypeName" + > +>; + +/** + * POST /subscriptions/{id} replaces the whole record, and Update.cs's model + * (SubscriptionUpdate) carries several fields this UI never shows (categoryId, + * aggregationTarget, temporary, …) — omitting them would silently reset them + * to their type default. So every write reads the current full record first + * and splices `changes` on top of it, mirroring writePartner()'s pattern. + * + * Secret adapter property values arrive masked as the literal string + * "__private__" (Get.cs's PrivateSentinel); passed straight through unedited, + * Update.cs's own merge logic restores the real stored value — this file + * never needs to know about the sentinel itself. + */ +async function applyChanges(id: number, current: RawSubscription, changes: UpdatableFields): Promise { + await post(`/subscriptions/${id}`, { + name: changes.name ?? current.name, + documentId: current.documentId, + partnerId: current.partnerId, + aggregationForId: current.aggregationForId, + categoryId: current.categoryId, + inactive: changes.enabled !== undefined ? !changes.enabled : current.inactive, + workGroupId: changes.workGroupId !== undefined ? changes.workGroupId : current.workGroupId, + // This UI only ever assigns a named policy, never an inline one. + retryPolicyId: changes.retryPolicyId !== undefined ? changes.retryPolicyId : current.retryPolicyId, + customRetryPolicy: null, + receiverId: changes.receiverId !== undefined ? changes.receiverId : current.receiverId, + receiverProperties: toKvArray(changes.receiverProperties ?? toRecord(current.receiverProperties)), + validatorId: changes.validatorId !== undefined ? changes.validatorId : current.validatorId, + validatorProperties: toKvArray(changes.validatorProperties ?? toRecord(current.validatorProperties)), + mapperId: changes.mapperId !== undefined ? changes.mapperId : current.mapperId, + mapperProperties: toKvArray(changes.mapperProperties ?? toRecord(current.mapperProperties)), + handlerId: changes.handlerId !== undefined ? changes.handlerId : current.handlerId, + handlerProperties: toKvArray(changes.handlerProperties ?? toRecord(current.handlerProperties)), + documentFilter: current.documentFilter ?? [], + matchExpression: + changes.matchExpression !== undefined ? toRawMatchExpression(changes.matchExpression) : current.matchExpression, + schedules: changes.schedules !== undefined ? toRawSchedules(changes.schedules) : (current.schedules ?? []), + responseSubscriptionId: + changes.responseIntegrationId !== undefined ? changes.responseIntegrationId : current.responseSubscriptionId, + responseMessageTypeName: + changes.responseMessageTypeName !== undefined ? changes.responseMessageTypeName : current.responseMessageTypeName, + temporary: current.temporary, + aggregationTarget: current.aggregationTarget, + pausedOn: current.pausedOn, + receiveOn: current.receiveOn, + aggregateOn: current.aggregateOn, + consecutiveFailures: current.consecutiveFailures, + lastException: current.lastException, + }); +} + +function toIntegrationRow( + raw: RawSubscription, + infoTypeById: Map, + partnerById: Map, +): IntegrationRow { + const type = toIntegrationType(raw.type); + const infoType = infoTypeById.get(raw.documentId); + const partner = raw.partnerId !== null ? partnerById.get(raw.partnerId) : undefined; + const schedules = toSchedules(raw.schedules); + return { + id: raw.id!, + name: raw.name, + type, + informationTypeId: raw.documentId, + informationTypeCode: infoType?.code ?? infoType?.name ?? "", + // Gateway-derived partners (GatewayApiCall/BusGateway) land in Batch 3. + partners: partner ? [{ id: partner.id, name: partner.name }] : [], + enabled: !raw.inactive, + paused: raw.pausedOn !== null, + isRunning: raw.isRunning ?? false, + consecutiveFailures: raw.consecutiveFailures ?? 0, + lastException: raw.lastException ?? null, + // Search.cs can't select Schedules in this joined query without + // breaking SQL translation (Postgres date_part type mismatch), so + // schedules is always empty here — showing "No schedule" would be + // actively wrong for a job that has one. Leave it unset instead. + scheduleSummary: + schedules.length > 0 && (type === "Receiving" || type === "Aggregation") + ? schedulesSummary(schedules) + : undefined, + nextReceiveOn: raw.receiveOn ?? null, + createdOn: "", + }; +} + +export const integrationMethods = { + async listIntegrations(): Promise { + const rows = await fetchAllRaw(); + return rows.map((raw) => ({ + id: raw.id!, + name: raw.name, + type: toIntegrationType(raw.type), + partnerIds: raw.partnerId !== null ? [raw.partnerId] : [], + informationTypeId: raw.documentId, + workGroupId: raw.workGroupId ?? null, + retryPolicyId: raw.retryPolicyId ?? null, + handlerId: raw.handlerId ?? null, + responseMessageTypeName: raw.responseMessageTypeName ?? null, + responseIntegrationId: raw.responseSubscriptionId ?? null, + // No backend endpoint indexes reference tokens, but the search rows carry + // every adapter property, so the scan costs nothing extra here. + ...scanReferenceTokens( + [ + raw.receiverProperties, + raw.validatorProperties, + raw.mapperProperties, + raw.handlerProperties, + ].flatMap((props) => (props ?? []).map((p) => p.value)), + ), + })); + }, + + async listIntegrationRows(): Promise { + const [rows, infoTypes, partners] = await Promise.all([ + fetchAllRaw(), + documentMethods.listInformationTypes(), + partnerMethods.listPartners(), + ]); + const infoTypeById = new Map(infoTypes.map((t) => [t.id, t])); + const partnerById = new Map(partners.map((p) => [p.id, p])); + return rows.map((raw) => toIntegrationRow(raw, infoTypeById, partnerById)); + }, + + async searchIntegrationRows(query: { + search: string; + type: IntegrationType | null; + informationTypeId?: number | null; + partnerId?: number | null; + inactive?: boolean | null; + offset: number; + limit: number; + }): Promise> { + const qs = buildListQuery({ + filters: [ + ["Name", SEARCHY_RULE.contains, query.search.trim()], + ["Type", SEARCHY_RULE.equalsTo, query.type ?? ""], + ["DocumentId", SEARCHY_RULE.equalsTo, query.informationTypeId ?? ""], + ["PartnerId", SEARCHY_RULE.equalsTo, query.partnerId ?? ""], + ["Inactive", SEARCHY_RULE.equalsTo, query.inactive == null ? "" : String(query.inactive)], + ], + sort: ["Name", SEARCHY_SORT.asc], + offset: query.offset, + limit: query.limit, + }); + const [res, infoTypes, partners] = await Promise.all([ + get>(`/subscriptions?${qs}`), + documentMethods.listInformationTypes(), + partnerMethods.listPartners(), + ]); + const infoTypeById = new Map(infoTypes.map((t) => [t.id, t])); + const partnerById = new Map(partners.map((p) => [p.id, p])); + return { + total: res.totalCount, + result: (res.result ?? []).map((raw) => toIntegrationRow(raw, infoTypeById, partnerById)), + }; + }, + + async getIntegration(id: number): Promise { + const [raw, apiGateways, busGateways, recentExchanges] = await Promise.all([ + fetchRaw(id), + gatewayMethods.listApiGateways(), + gatewayMethods.listBusGateways(), + exchangeMethods.searchExchanges({ integrationId: id, offset: 0, limit: 8 }), + ]); + const infoType = await documentMethods.getInformationType(raw.documentId).catch(() => null); + return { + ...toIntegration(raw, id), + informationTypeCode: infoType?.code ?? infoType?.name ?? "", + informationTypeName: infoType?.name ?? "", + apiGatewayAttachments: apiGateways.flatMap((g) => + g.attachments + .filter((a) => a.integrationId === id) + .map((a) => ({ + gatewayId: g.id, + gatewayName: g.name, + urlName: g.urlName, + partnerId: a.partnerId, + partnerName: a.partnerName, + })), + ), + busGatewayRoutes: busGateways.flatMap((g) => + g.routes + .filter((r) => r.integrationId === id) + .map((r) => ({ gatewayId: g.id, gatewayName: g.name, partnerId: r.partnerId, partnerName: r.partnerName })), + ), + recentExchanges: recentExchanges.result.map((x) => ({ + id: x.id, + partnerName: x.partnerName ?? undefined, + informationTypeCode: x.informationTypeCode, + status: x.status, + on: x.startedOn, + promotedProperties: x.promotedProperties, + })), + // Populated once notifiers and the trail (a distinct audit-log endpoint, + // deferred alongside the mapper editor/aggregation) are wired. + watchingNotifiers: [], + trail: [], + }; + }, + + async createIntegration(input: { + type: IntegrationType; + name: string; + informationTypeId: number; + /** Required by the types that carry their own partner — Internal and ApiCall. */ + partnerId?: number | null; + receiverId?: string | null; + receiverProperties?: Record; + validatorId?: string | null; + validatorProperties?: Record; + mapperId?: string | null; + mapperProperties?: Record; + handlerId?: string | null; + handlerProperties?: Record; + schedules?: Schedule[]; + retryPolicyId?: number | null; + responseIntegrationId?: number | null; + responseMessageTypeName?: string | null; + enabled?: boolean; + }): Promise { + // One call, one transaction. This used to be a POST followed by a PATCH, + // because create accepted only the name/type/document — and since the POST + // committed on its own, a rejected PATCH left an empty subscription behind. + const id = await post("/subscriptions", { + name: input.name, + documentId: input.informationTypeId, + type: input.type, + partnerId: input.partnerId ?? null, + aggregationForId: null, + receiverId: input.receiverId ?? null, + receiverProperties: toKvArray(input.receiverProperties ?? {}), + validatorId: input.validatorId ?? null, + validatorProperties: toKvArray(input.validatorProperties ?? {}), + mapperId: input.mapperId ?? null, + mapperProperties: toKvArray(input.mapperProperties ?? {}), + handlerId: input.handlerId ?? null, + handlerProperties: toKvArray(input.handlerProperties ?? {}), + documentFilter: [], + // Undefined rather than [] when there is no schedule: an empty array on a + // Receiving subscription is rejected, and a job created without one is a + // legitimate (if idle) thing to have. + schedules: input.schedules?.length ? toRawSchedules(input.schedules) : undefined, + retryPolicyId: input.retryPolicyId ?? null, + customRetryPolicy: null, + responseSubscriptionId: input.responseIntegrationId ?? null, + responseMessageTypeName: input.responseMessageTypeName ?? null, + inactive: !(input.enabled ?? false), + }); + return toIntegration(await fetchRaw(id), id); + }, + + async updateIntegration(id: number, changes: UpdatableFields): Promise { + const current = await fetchRaw(id); + await applyChanges(id, current, changes); + return toIntegration(await fetchRaw(id), id); + }, + + async deleteIntegration(id: number): Promise { + await request(`/subscriptions/${id}`, { method: "DELETE" }); + }, + + async pauseIntegration(id: number): Promise { + await post(`/subscriptions/${id}/pause`, {}); + return toIntegration(await fetchRaw(id), id); + }, + + async receiveNow(id: number): Promise { + await post(`/subscriptions/${id}/receivenow`, {}); + return toIntegration(await fetchRaw(id), id); + }, + + listIntegrationRuns(id: number, limit = 20): Promise { + return get(`/subscriptions/runs?subscriptionId=${id}&limit=${limit}`); + }, + + async searchReceiveAttempts( + subscriptionId: number, + query: { outcome: ReceiveOutcome | null; offset: number; limit: number }, + ): Promise> { + const params = new URLSearchParams({ + subscriptionId: String(subscriptionId), + offset: String(query.offset), + limit: String(query.limit), + }); + if (query.outcome) params.set("outcome", query.outcome); + const res = await get>(`/subscriptions/receiveattempts?${params.toString()}`); + return { + total: res.totalCount, + result: (res.result ?? []).map((a) => ({ + id: a.id, + startedOn: a.startedOn, + finishedOn: a.finishedOn, + outcome: toReceiveOutcome(a.outcome), + errorMessage: a.errorMessage, + exchanges: a.exchanges.map((x) => ({ + id: x.id, + status: deriveStatus(x), + promotedProperties: x.promotedProperties, + })), + })), + }; + }, + + async listLastRuns(): Promise { + const rows = + await get<(Omit & { subscriptionId: number })[]>( + "/subscriptions/lastruns", + ); + return rows.map(({ subscriptionId, ...run }) => ({ ...run, integrationId: subscriptionId })); + }, + + async listScheduleHealth(): Promise { + const rows = + await get<(Omit & { subscriptionId: number })[]>( + "/subscriptions/schedulehealth", + ); + return rows.map(({ subscriptionId, ...health }) => ({ ...health, integrationId: subscriptionId })); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/mappers.ts b/SW.Bitween.Web/ClientApp/src/api/http/mappers.ts new file mode 100644 index 00000000..68ebcf02 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/mappers.ts @@ -0,0 +1,18 @@ +import type { ApiClient } from "../client"; +import { post } from "./request"; + +interface RawMapperPreviewResponse { + outputJson: string | null; + error: string | null; +} + +export const mapperMethods = { + async previewMapping(input: { + scribanTemplate: string; + inputJson: string; + partnerId?: number | null; + }): Promise<{ outputJson: string | null; error: string | null }> { + const res = await post("/mappers", input); + return { outputJson: res.outputJson ?? null, error: res.error ?? null }; + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/matchExpression.ts b/SW.Bitween.Web/ClientApp/src/api/http/matchExpression.ts new file mode 100644 index 00000000..b05102b9 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/matchExpression.ts @@ -0,0 +1,57 @@ +import type { MatchGroup, MatchNode } from "../types"; + +/** + * The backend's match tree is strictly binary (and/or each take exactly two + * operands) and uses snake_case type discriminators, unlike the frontend's + * n-ary MatchGroup. Shared by Subscriptions and BusGatewayRoute — both use the + * exact same backend type (`IPropertyMatchSpecification`). + */ +export type RawMatchSpec = + | { type: "one_of" | "not_one_of"; path: string; values: string[] | null } + | { type: "and" | "or"; left: RawMatchSpec; right: RawMatchSpec }; + +/** + * Fold the backend's binary and/or tree into the frontend's n-ary MatchGroup, + * flattening runs of the same operator so a flat group round-trips back flat + * instead of as a deeply right-nested tree. + */ +function toMatchNode(spec: RawMatchSpec): MatchNode { + if (!("left" in spec)) { + return { op: spec.type === "one_of" ? "oneOf" : "notOneOf", path: spec.path, values: spec.values ?? [] }; + } + const op = spec.type; + const children: MatchNode[] = []; + const collect = (s: RawMatchSpec) => { + if (s.type === op) { + collect(s.left); + collect(s.right); + } else { + children.push(toMatchNode(s)); + } + }; + collect(spec); + return { op, children }; +} + +export const toMatchGroup = (spec: RawMatchSpec | null): MatchGroup | null => { + if (!spec) return null; + const node = toMatchNode(spec); + // The backend can't represent a single-condition group (and/or always need + // two operands), so a lone condition arrives unwrapped and must be rewrapped + // here to satisfy the "root is always a group" contract. + return "children" in node ? node : { op: "and", children: [node] }; +}; + +/** Unfold an n-ary MatchGroup into the backend's binary tree, right-associatively. */ +function toBackendNode(node: MatchNode): RawMatchSpec | null { + if ("path" in node) { + return { type: node.op === "oneOf" ? "one_of" : "not_one_of", path: node.path, values: node.values }; + } + const parts = node.children.map(toBackendNode).filter((s): s is RawMatchSpec => s !== null); + if (parts.length === 0) return null; // empty group — matches everything, i.e. no constraint + if (parts.length === 1) return parts[0]; + return parts.reduceRight((right, left) => ({ type: node.op, left, right })); +} + +export const toRawMatchExpression = (group: MatchGroup | null): RawMatchSpec | null => + group ? toBackendNode(group) : null; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts b/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts new file mode 100644 index 00000000..b55a54fb --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts @@ -0,0 +1,142 @@ +import type { ApiClient } from "../client"; +import { ApiRequestError, type NotificationEntry, type Notifier, type NotifierDetail, type Paged } from "../types"; +import { get, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; + +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawKeyAndValue { + key: string; + value: string; +} +interface RawNotifier { + id: number; + name: string; + inactive: boolean; + handlerId: string | null; + handlerProperties: RawKeyAndValue[] | null; + runOnSuccessfulResult: boolean; + runOnBadResult: boolean; + runOnFailedResult: boolean; + runOnSubscriptions: { id: number; name: string | null }[] | null; +} +/** Shape of a search-endpoint row — lighter than `RawNotifier`, no adapter properties. */ +interface RawNotifierRow { + id: number; + name: string; + inactive: boolean | null; + handlerId: string | null; + runOnSuccessfulResult: boolean | null; + runOnBadResult: boolean | null; + runOnFailedResult: boolean | null; + runOnSubscriptions: number[] | null; +} +interface RawNotification { + xchangeId: string; + success: boolean; + exception: string | null; + finishedOn: string; +} +// Empty-valued properties are dropped: an adapter property with no value means +// "not set", and keeping it would make a freshly-cleared field compare unequal to +// stored data that never had the key — leaving the Save bar up after an undo. +const toRecord = (kvs: RawKeyAndValue[] | null): Record => + Object.fromEntries((kvs ?? []).filter((kv) => kv.value !== "").map((kv) => [kv.key, kv.value])); +const toKvArray = (record: Record): RawKeyAndValue[] => + Object.entries(record).map(([key, value]) => ({ key, value })); + +const toNotifier = (r: RawNotifier): Notifier => ({ + id: r.id, + name: r.name, + enabled: !r.inactive, + onFailed: r.runOnFailedResult, + onBadResult: r.runOnBadResult, + onSuccess: r.runOnSuccessfulResult, + channelId: r.handlerId ?? "", + channelProperties: toRecord(r.handlerProperties), + integrationIds: (r.runOnSubscriptions ?? []).map((s) => s.id), + createdOn: "", +}); + +async function fetchRecentNotifications(notifierName: string): Promise { + const res = await get>( + `/notifications?filter=${encodeURIComponent(`NotifierName:1:${notifierName}`)}`, + ); + return (res.result ?? []).map((n) => ({ + xchangeId: n.xchangeId, + success: n.success, + exception: n.exception ?? undefined, + on: n.finishedOn, + })); +} + +async function fetchDetail(id: number): Promise { + const raw = await get(`/notifiers/${id}`); + if (!raw) throw new ApiRequestError("NOT_FOUND", "This notifier no longer exists."); + const notifier = toNotifier(raw); + return { ...notifier, recentNotifications: await fetchRecentNotifications(notifier.name) }; +} + +export const notifierMethods = { + async searchNotifiers(query: { search: string; offset: number; limit: number }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const res = await get>(`/notifiers?${qs}`); + return { + total: res.totalCount, + result: (res.result ?? []).map((r) => ({ + id: r.id, + name: r.name, + enabled: !r.inactive, + onFailed: !!r.runOnFailedResult, + onBadResult: !!r.runOnBadResult, + onSuccess: !!r.runOnSuccessfulResult, + channelId: r.handlerId ?? "", + channelProperties: {}, + integrationIds: r.runOnSubscriptions ?? [], + createdOn: "", + })), + }; + }, + + getNotifier: fetchDetail, + + async createNotifier({ name }: { name: string }): Promise { + const id = await post("/notifiers", { name }); + return { + id, + name, + enabled: true, + onFailed: false, + onBadResult: false, + onSuccess: false, + channelId: "", + channelProperties: {}, + integrationIds: [], + createdOn: "", + }; + }, + + async updateNotifier(id: number, changes: Omit): Promise { + await post(`/notifiers/${id}`, { + name: changes.name, + runOnSuccessfulResult: changes.onSuccess, + runOnBadResult: changes.onBadResult, + runOnFailedResult: changes.onFailed, + handlerId: changes.channelId, + inactive: !changes.enabled, + handlerProperties: toKvArray(changes.channelProperties), + runOnSubscriptions: changes.integrationIds.map((subscriptionId) => ({ id: subscriptionId })), + }); + return { id, createdOn: "", ...changes }; + }, + + async deleteNotifier(id: number): Promise { + await request(`/notifiers/${id}`, { method: "DELETE" }); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/partners.ts b/SW.Bitween.Web/ClientApp/src/api/http/partners.ts new file mode 100644 index 00000000..567da971 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/partners.ts @@ -0,0 +1,183 @@ +import type { ApiClient } from "../client"; +import { ApiRequestError, type Paged, type Partner, type PartnerDetail, type PartnerRow } from "../types"; +import { exchangeMethods } from "./exchanges"; +import { gatewayMethods } from "./gateways"; +import { matchSummary } from "../../lib/match"; +import { get, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; + +// The built-in SYSTEM partner (Partner.SystemId) can't be renamed or deleted. +const SYSTEM_PARTNER_ID = 1; + +// ——— backend shapes (camelCase over the wire) ——— +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawPartnerRow { + id: number; + name: string; + subscriptionsCount: number; + keys: number; + propertyKeys: string[] | null; +} +interface RawKeyAndValue { + key: string; + value: string; +} +interface RawPartnerDetail { + name: string; + apiCredentials: RawKeyAndValue[] | null; + adapterProperties: Record | null; +} + +// GET masks keys as `...(hidden)`; recover the visible prefix. +const MASK_SUFFIX = "...(hidden)"; +const keyPrefixOf = (masked: string): string => + masked.endsWith(MASK_SUFFIX) ? masked.slice(0, -MASK_SUFFIX.length) : masked; + +async function requireDetail(id: number): Promise { + const d = await get(`/partners/${id}`); + if (!d) throw new ApiRequestError("NOT_FOUND", "This partner no longer exists."); + return d; +} + +/** + * `POST /partners/{id}` REPLACES the whole credential set, so every write must + * carry the full list. The backend matches credentials by name and keeps the + * stored secret, so re-sending the masked values is safe (verified against the + * live DB); omitting one revokes it. `mutate` produces the next list. + */ +async function writePartner( + id: number, + d: RawPartnerDetail, + patch: { name?: string; adapterProperties?: Record }, + mutate: (creds: RawKeyAndValue[]) => RawKeyAndValue[] = (c) => c, +): Promise { + const name = patch.name ?? d.name; + const adapterProperties = patch.adapterProperties ?? d.adapterProperties ?? {}; + const apiCredentials = mutate((d.apiCredentials ?? []).map((c) => ({ key: c.key, value: c.value }))); + await post(`/partners/${id}`, { name, adapterProperties, apiCredentials }); + return { id, name, adapterProperties, isSystem: id === SYSTEM_PARTNER_ID, createdOn: "" }; +} + +export const partnerMethods = { + async listPartners(): Promise { + const res = await get>("/partners"); + return (res.result ?? []).map((p) => ({ + id: p.id, + name: p.name, + // The list endpoint sends property names, never their values — they can be + // secrets. Anything needing values must fetch the partner's detail. + adapterProperties: {}, + propertyKeys: p.propertyKeys ?? [], + isSystem: p.id === SYSTEM_PARTNER_ID, + createdOn: "", + credentialCount: p.keys, + usedByCount: p.subscriptionsCount, + })); + }, + + async searchPartners(query: { search: string; offset: number; limit: number }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const res = await get>(`/partners?${qs}`); + return { + total: res.totalCount, + result: (res.result ?? []).map((p) => ({ + id: p.id, + name: p.name, + adapterProperties: {}, + propertyKeys: p.propertyKeys ?? [], + isSystem: p.id === SYSTEM_PARTNER_ID, + createdOn: "", + credentialCount: p.keys, + usedByCount: p.subscriptionsCount, + })), + }; + }, + + /** Light single-field fetch for the mapper editor's test-partner selector — avoids getPartner's gateway/exchange lookups. */ + async getPartnerAdapterProperties(id: number): Promise> { + const d = await requireDetail(id); + return d.adapterProperties ?? {}; + }, + + async getPartner(id: number): Promise { + const [d, apiGateways, busGateways, recentExchanges] = await Promise.all([ + requireDetail(id), + gatewayMethods.listApiGateways(), + gatewayMethods.listBusGateways(), + exchangeMethods.searchExchanges({ partnerId: id, offset: 0, limit: 8 }), + ]); + return { + id, + name: d.name, + adapterProperties: d.adapterProperties ?? {}, + isSystem: id === SYSTEM_PARTNER_ID, + createdOn: "", + apiCredentials: (d.apiCredentials ?? []).map((c) => ({ + name: c.key, + keyPrefix: keyPrefixOf(c.value), + createdOn: "", + })), + apiGateways: apiGateways + .filter((g) => g.attachments.some((a) => a.partnerId === id)) + .map((g) => ({ gatewayId: g.id, gatewayName: g.name, urlName: g.urlName })), + busGatewayRoutes: busGateways.flatMap((g) => + g.routes + .filter((r) => r.partnerId === id) + .map((r) => ({ gatewayId: g.id, gatewayName: g.name, matchExpression: matchSummary(r.matchExpression) })), + ), + recentExchanges: recentExchanges.result.map((x) => ({ + id: x.id, + informationTypeCode: x.informationTypeCode, + status: x.status, + on: x.startedOn, + promotedProperties: x.promotedProperties, + })), + }; + }, + + async createPartner({ + name, + adapterProperties = {}, + }: { + name: string; + adapterProperties?: Record; + }): Promise { + // One call: Partners/Create applies AdapterProperties in the same transaction + // as the insert, so a partner is never created without the values its adapters + // are about to resolve. + const id = await post("/partners", { name, adapterProperties }); + return { id, name, adapterProperties, isSystem: false, createdOn: "" }; + }, + + async updatePartner( + id: number, + changes: { name?: string; adapterProperties?: Record }, + ): Promise { + return writePartner(id, await requireDetail(id), changes); + }, + + async deletePartner(id: number): Promise { + await request(`/partners/${id}`, { method: "DELETE" }); + }, + + async addPartnerCredential(id: number, name: string): Promise<{ key: string }> { + const key = await get("/partners/generatekey"); + const d = await requireDetail(id); + if ((d.apiCredentials ?? []).some((c) => c.key.toLowerCase() === name.trim().toLowerCase())) + throw new ApiRequestError("NAME_TAKEN", "This partner already has a key with that name."); + await writePartner(id, d, {}, (creds) => [...creds, { key: name.trim(), value: key }]); + return { key }; + }, + + async revokePartnerCredential(id: number, name: string): Promise { + const d = await requireDetail(id); + await writePartner(id, d, {}, (creds) => creds.filter((c) => c.key !== name)); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/queueHealth.ts b/SW.Bitween.Web/ClientApp/src/api/http/queueHealth.ts new file mode 100644 index 00000000..9a5aad8a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/queueHealth.ts @@ -0,0 +1,148 @@ +import type { ApiClient } from "../client"; +import type { QueueHealthSnapshot, QueueLane, QueueSeverity, UnattendedQueue } from "../types"; +import { get } from "./request"; + +// ——— backend shapes (camelCase over the wire; severities are PascalCase strings) ——— +type RawSeverity = "Info" | "Warning" | "Critical"; +interface RawSummary { + totalConsumers: number; + unhealthyConsumers: number; + disconnectedConsumers: number; + totalQueueDepth: number; + totalRetryBacklog: number; + totalDeadLetterBacklog: number; + totalIncomingRate: number; + totalAckRate: number; + lastUpdatedUtc: string; +} +interface RawConsumer { + name: string; + messageName: string; + queueName: string; + lane: QueueLane; + title: string; + workGroupId: number | null; + informationTypeId: number | null; + totalNodes: number; + processingCount: number; + queueCount: number; + retryCount: number; + failedCount: number; + priority: number; + prefetch: number; + incomingRate: number; + ackRate: number; + isBackpressured: boolean; + healthStatus: RawSeverity; +} +interface RawRetryAnalysis { + consumerName: string; + queueName: string; + retryBacklog: number; + incomingRate: number; + ackRate: number; + severity: RawSeverity; +} +interface RawDeadLetter { + consumerName: string; + deadLetterQueueName: string; + deadLetterCount: number; + lastExceptionType: string | null; + lastExceptionMessage: string | null; + lastFailedAt: string | null; +} +interface RawAlert { + severity: RawSeverity; + title: string; + detail: string; + queueName: string; + timestampUtc: string; +} + +const toSeverity = (s: RawSeverity): QueueSeverity => (s === "Critical" ? "critical" : s === "Warning" ? "warning" : "healthy"); + +export const queueHealthMethods = { + async getQueueHealth(): Promise { + const [summary, consumers, retries, deadLetters, alerts, unattended] = await Promise.all([ + get("/ops/summary"), + get("/ops/consumers"), + get("/ops/retries"), + get("/ops/deadletters"), + get("/ops/alerts"), + // The one call that asks the broker for a list rather than for statistics. Newest and + // least proven thing on an on-call page, so it isn't allowed to take the rest with it. + get("/ops/unattendedqueues").catch(() => []), + ]); + + // The retry and dead-letter rows carry `{queueName}.retry` / `.bad` and no lane of + // their own, so they're matched back to a consumer row by trimming the suffix. + const titleByQueue = new Map(consumers.map((c) => [c.queueName, c.title])); + const titleOf = (queueName: string, suffix: string): string => + titleByQueue.get(queueName.replace(new RegExp(`\\${suffix}$`), "")) ?? queueName; + + return { + summary: { + totalConsumers: summary.totalConsumers, + unhealthyConsumers: summary.unhealthyConsumers, + disconnectedConsumers: summary.disconnectedConsumers, + totalQueueDepth: summary.totalQueueDepth, + totalRetryBacklog: summary.totalRetryBacklog, + totalDeadLetterBacklog: summary.totalDeadLetterBacklog, + totalIncomingRate: summary.totalIncomingRate, + totalAckRate: summary.totalAckRate, + lastUpdated: summary.lastUpdatedUtc, + }, + consumers: consumers.map((c) => ({ + name: c.name, + messageName: c.messageName, + queueName: c.queueName, + lane: c.lane, + title: c.title, + workGroupId: c.workGroupId, + informationTypeId: c.informationTypeId, + totalNodes: c.totalNodes, + processingCount: c.processingCount, + queueCount: c.queueCount, + retryCount: c.retryCount, + failedCount: c.failedCount, + priority: c.priority, + prefetch: c.prefetch, + incomingRate: c.incomingRate, + ackRate: c.ackRate, + isBackpressured: c.isBackpressured, + health: toSeverity(c.healthStatus), + })), + retryBacklog: retries.map((r) => ({ + consumerName: r.consumerName, + title: titleOf(r.queueName, ".retry"), + queueName: r.queueName, + retryBacklog: r.retryBacklog, + incomingRate: r.incomingRate, + ackRate: r.ackRate, + severity: toSeverity(r.severity), + })), + deadLetters: deadLetters.map((d) => ({ + consumerName: d.consumerName, + title: titleOf(d.deadLetterQueueName, ".bad"), + queueName: d.deadLetterQueueName, + count: d.deadLetterCount, + lastExceptionType: d.lastExceptionType, + lastExceptionMessage: d.lastExceptionMessage, + lastFailedAt: d.lastFailedAt, + })), + unattended, + // The frontend's alert severity is narrower ("warning"|"critical") than + // the backend's ("Info"|"Warning"|"Critical") — informational alerts + // aren't alerts from the UI's point of view, so drop them. + alerts: alerts + .filter((a) => a.severity !== "Info") + .map((a) => ({ + severity: a.severity === "Critical" ? ("critical" as const) : ("warning" as const), + title: a.title, + detail: a.detail, + queueName: a.queueName, + on: a.timestampUtc, + })), + }; + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/references.ts b/SW.Bitween.Web/ClientApp/src/api/http/references.ts new file mode 100644 index 00000000..094f4e2a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/references.ts @@ -0,0 +1,59 @@ +/** + * Reference tokens inside adapter property values. + * + * Adapters can carry `{{globals..}}` and `{{partner.}}` in any + * property value; the backend substitutes them at run time + * (`Helpers/StartupValuesFiller.cs`). Nothing indexes them, so "who references + * this value set / partner property?" can only be answered by scanning the + * properties the same way the resolver parses them. + * + * The resolver splits the globals token on the **first** "." only and puts no + * character-class restriction on either half, so these patterns are equally + * permissive rather than assuming a slug charset. It also matches the prefix + * and looks up ids and keys with `OrdinalIgnoreCase` — hence the `i` flag, and + * why callers must compare what comes back case-insensitively. + */ +const GLOBAL_TOKEN_RE = /\{\{globals\.([^.]+)\.([^}]+)\}\}/gi; +const PARTNER_TOKEN_RE = /\{\{partner\.([^}]+)\}\}/gi; + +export interface ReferenceTokens { + /** Global value references, grouped by the set id as written in the token. */ + globals: { setId: string; keys: string[] }[]; + /** Partner property keys referenced by `{{partner.KEY}}`. */ + partnerPropKeys: string[]; +} + +export function scanReferenceTokens(values: (string | null | undefined)[]): ReferenceTokens { + const globals = new Map>(); + const partnerPropKeys = new Set(); + + for (const value of values) { + if (!value) continue; + for (const [, setId, key] of value.matchAll(GLOBAL_TOKEN_RE)) { + const keys = globals.get(setId) ?? new Set(); + keys.add(key); + globals.set(setId, keys); + } + for (const [, key] of value.matchAll(PARTNER_TOKEN_RE)) partnerPropKeys.add(key); + } + + return { + globals: [...globals].map(([setId, keys]) => ({ setId, keys: [...keys] })), + partnerPropKeys: [...partnerPropKeys], + }; +} + +/** Case-insensitive, matching how the resolver compares set ids and keys. */ +const eq = (a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: "accent" }) === 0; + +export const referencesGlobal = ( + refs: Pick, + setId: string, + key?: string, +): boolean => + refs.globals.some( + (g) => eq(g.setId, setId) && (key === undefined || g.keys.some((k) => eq(k, key))), + ); + +export const referencesPartnerProp = (refs: Pick, key: string): boolean => + refs.partnerPropKeys.some((k) => eq(k, key)); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/request.ts b/SW.Bitween.Web/ClientApp/src/api/http/request.ts new file mode 100644 index 00000000..67214946 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/request.ts @@ -0,0 +1,154 @@ +import { ApiRequestError } from "../types"; + +/** + * All endpoints live under /api (UrlPrefix="api"). + * The SPA is served from the same origin, so this stays relative and cookies flow. + */ +export const API_BASE = "/api"; + +/** The Jwt is kept in localStorage; the refresh token is an HttpOnly cookie JS never sees. */ +const TOKEN_KEY = "access_token"; + +export const getToken = (): string | null => localStorage.getItem(TOKEN_KEY); +export const setToken = (jwt: string): void => localStorage.setItem(TOKEN_KEY, jwt); +export const clearToken = (): void => localStorage.removeItem(TOKEN_KEY); + +/** Backend serializes camelCase; login returns `{ jwt }`. */ +const readJwt = (data: unknown): string | null => + (data as { jwt?: string; Jwt?: string })?.jwt ?? (data as { Jwt?: string })?.Jwt ?? null; + +let refreshInFlight: Promise | null = null; + +/** + * Silent refresh: POST /accounts/login with an empty body — the HttpOnly + * refresh_token cookie alone re-issues a Jwt. Returns the new token, or null + * when the cookie is missing/expired. Bypasses `request()` to avoid recursion, + * and dedupes concurrent callers behind one in-flight promise. + */ +export function silentRefresh(): Promise { + if (!refreshInFlight) { + refreshInFlight = (async () => { + try { + const res = await fetch(`${API_BASE}/accounts/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: "{}", + }); + if (!res.ok) return null; + const jwt = readJwt(await res.json().catch(() => null)); + if (jwt) setToken(jwt); + else clearToken(); + return jwt; + } catch { + return null; + } + })(); + void refreshInFlight.finally(() => { + refreshInFlight = null; + }); + } + return refreshInFlight; +} + +/** Pull a human message + best-effort code out of a backend error response. */ +async function toApiError(res: Response): Promise { + const text = await res.text().catch(() => ""); + let body: unknown = text; + try { + body = text ? JSON.parse(text) : ""; + } catch { + /* leave as text */ + } + + // 404 → the message is a bare JSON string. + if (typeof body === "string" && body) + return new ApiRequestError(res.status === 404 ? "NOT_FOUND" : "ERROR", body); + + // Framework-level errors (415, unhandled 500, …) come as ASP.NET ProblemDetails: + // { type, title, status, traceId }. Prefer its human `title`. + if (body && typeof body === "object" && "title" in body && "status" in body) { + const pd = body as { title?: string; status?: number }; + return new ApiRequestError(`HTTP_${pd.status ?? res.status}`, pd.title || `Request failed (${res.status}).`); + } + + // 400 → ASP.NET SerializableError: { key: [msg, ...] } (key is the validation + // code for SWValidationException, or the exception type name otherwise). + if (body && typeof body === "object") { + const [code, value] = Object.entries(body as Record)[0] ?? []; + const message = Array.isArray(value) ? String(value[0]) : String(value ?? ""); + if (code) return new ApiRequestError(code, message || "Request failed."); + } + + return new ApiRequestError("ERROR", `Request failed (${res.status}).`); +} + +export interface RequestOptions { + method?: "GET" | "POST" | "DELETE"; + body?: unknown; + /** Internal: prevents the 401 → refresh → retry loop from recursing. */ + _retried?: boolean; +} + +/** + * The one fetch helper every wired method goes through: prefixes the base, + * attaches `Authorization: Bearer `, includes credentials so the refresh + * cookie rides along, and on 401 attempts a single silent refresh + retry. + */ +export async function request(path: string, opts: RequestOptions = {}): Promise { + const token = getToken(); + const method = opts.method ?? "GET"; + // Backend command handlers (POST) bind a JSON body, so they always need + // `Content-Type: application/json` — even a body-less command like logout. + // Without it the framework rejects the call with 415 before the handler runs; + // an empty `{}` satisfies it. + const sendJson = method === "POST"; + const res = await fetch(`${API_BASE}${path}`, { + method, + credentials: "include", + headers: { + ...(sendJson ? { "Content-Type": "application/json" } : {}), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: sendJson ? JSON.stringify(opts.body ?? {}) : undefined, + }); + + if (res.status === 401 && !opts._retried) { + const refreshed = await silentRefresh(); + if (refreshed) return request(path, { ...opts, _retried: true }); + clearToken(); + throw new ApiRequestError("UNAUTHENTICATED", "Your session has ended. Please sign in again."); + } + + if (!res.ok) throw await toApiError(res); + + if (res.status === 204) return undefined as T; + const text = await res.text(); + if (!text) return undefined as T; + // Some endpoints return a bare string as text/plain (e.g. /partners/generatekey), + // which isn't valid JSON — only parse when the response actually is JSON. + const isJson = res.headers.get("content-type")?.includes("application/json") ?? false; + return (isJson ? JSON.parse(text) : text) as T; +} + +export const get = (path: string): Promise => request(path); +export const post = (path: string, body?: unknown): Promise => + request(path, { method: "POST", body }); + +/** + * For a secondary read that only enriches a page — the "used by" counts, which come from another + * area's list. Those are permission-guarded in their own right, so a role that can see this page + * but not that area would otherwise take the whole page down with it. The enrichment is worth + * losing; the page isn't. Only refusals are swallowed, so a real outage still surfaces. + */ +export async function getEnrichment(path: string, fallback: T): Promise { + try { + return await get(path); + } catch (e) { + // A refusal for *this* read only. UNAUTHENTICATED deliberately isn't swallowed: that one means + // the session itself is gone, and the app needs to hear about it. + const code = e instanceof ApiRequestError ? e.code : ""; + if (code === "HTTP_401" || code === "HTTP_403") return fallback; + throw e; + } +} diff --git a/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts b/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts new file mode 100644 index 00000000..ade1a22e --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts @@ -0,0 +1,387 @@ +import type { ApiClient } from "../client"; +import { + ApiRequestError, + type IntegrationType, + type RetryAlertConfig, + type RetryAlertLevel, + type RetryAttempts, + type RetryDelay, + type RetryGroup, + type RetryPolicy, + type RetryPolicyDetail, + type RetryPolicyListRow, + type RetryResultType, + type RetryTestAttempt, + type RetryUsageRow, + type Paged, +} from "../types"; +import { get, getEnrichment, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; + +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawRetryPolicyRow { + id: number; + name: string; + groupCount: number; +} +interface RawRetryPolicy { + name: string; + groups: RawRetryGroup[] | null; + alertHandlerId: string | null; + alertHandlerProperties: Record | null; +} +interface RawSubscriptionRef { + id: number; + name: string; + type: number | string; +} + +const SUB_TYPE_BY_NUM: Record = { + 1: "Internal", + 2: "ApiCall", + 4: "Receiving", + 8: "Aggregation", + 16: "GatewayApiCall", + 32: "BusGateway", +}; +const INTEGRATION_TYPES: IntegrationType[] = [ + "Receiving", + "GatewayApiCall", + "BusGateway", + "Internal", + "ApiCall", + "Aggregation", +]; +/** Enums may arrive as the numeric value or the name in any case. */ +const toIntegrationType = (t: number | string): IntegrationType => { + if (typeof t === "number") return SUB_TYPE_BY_NUM[t] ?? "Internal"; + return INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; +}; + +async function fetchSubscriptionsByRetryPolicy(retryPolicyId: number): Promise { + const res = await get>( + `/subscriptions?filter=${encodeURIComponent(`RetryPolicyId:1:${retryPolicyId}`)}`, + ); + return res.result ?? []; +} + +// The backend's DelayStrategy keeps durations in milliseconds; the UI works in seconds. +type RawDelayStrategy = + | { type: "fixed"; delayMs: number } + | { type: "linear"; initialDelayMs: number; incrementMs: number } + | { type: "exponential"; initialDelayMs: number; multiplier: number; maxDelayMs: number }; +interface RawRetryBudget { + maxAttemptsPerError: number; + maxAttemptsTotal: number; + delayStrategy: RawDelayStrategy; +} +interface RawRetryGroup extends Omit { + budget?: RawRetryBudget | null; + alertHandlerProperties?: Record | null; +} +interface RawTestAttempt { + attemptNumber: number; + matchedGroupName: string | null; + shouldRetry: boolean; + delaySeconds: number | null; + reason: string; +} + +const toDelay = (d: RawDelayStrategy): RetryDelay => { + switch (d.type) { + case "fixed": + return { type: "fixed", delaySeconds: d.delayMs / 1000 }; + case "linear": + return { type: "linear", initialSeconds: d.initialDelayMs / 1000, incrementSeconds: d.incrementMs / 1000 }; + case "exponential": + return { + type: "exponential", + initialSeconds: d.initialDelayMs / 1000, + multiplier: d.multiplier, + maxSeconds: d.maxDelayMs / 1000, + }; + } +}; + +const toRawDelay = (d: RetryDelay): RawDelayStrategy => { + switch (d.type) { + case "fixed": + return { type: "fixed", delayMs: Math.round(d.delaySeconds * 1000) }; + case "linear": + return { + type: "linear", + initialDelayMs: Math.round(d.initialSeconds * 1000), + incrementMs: Math.round(d.incrementSeconds * 1000), + }; + case "exponential": + return { + type: "exponential", + initialDelayMs: Math.round(d.initialSeconds * 1000), + multiplier: d.multiplier, + maxDelayMs: Math.round(d.maxSeconds * 1000), + }; + } +}; + +const toGroup = (g: RawRetryGroup): RetryGroup => ({ + ...g, + budget: g.budget + ? { + maxAttemptsPerError: g.budget.maxAttemptsPerError, + maxAttemptsTotal: g.budget.maxAttemptsTotal, + delay: toDelay(g.budget.delayStrategy), + } + : undefined, + // Named rather than left to the spread: a policy saved from an older client has no + // alert fields at all, and an undefined mode would compare unequal to "Inherit" and + // leave the Save bar up on a page nobody had edited. + alertMode: g.alertMode ?? "Inherit", + alertHandlerId: g.alertHandlerId ?? null, + alertHandlerProperties: g.alertHandlerProperties ?? {}, +}); + +const toRawGroup = (g: RetryGroup): RawRetryGroup => ({ + ...g, + budget: g.budget + ? { + maxAttemptsPerError: g.budget.maxAttemptsPerError, + maxAttemptsTotal: g.budget.maxAttemptsTotal, + delayStrategy: toRawDelay(g.budget.delay), + } + : undefined, +}); + +async function fetchDetail(id: number): Promise { + const [r, subs] = await Promise.all([ + get(`/retrypolicies/${id}`), + fetchSubscriptionsByRetryPolicy(id), + ]); + if (!r) throw new ApiRequestError("NOT_FOUND", "This retry policy no longer exists."); + return { + id, + name: r.name, + groups: (r.groups ?? []).map(toGroup), + createdOn: "", + alertHandlerId: r.alertHandlerId ?? null, + alertHandlerProperties: r.alertHandlerProperties ?? {}, + integrations: subs.map((s) => ({ id: s.id, name: s.name, type: toIntegrationType(s.type) })), + }; +} + +interface RawUsageRow { + subscriptionId: number; + subscriptionName: string; + groupId: string; + groupName: string; + attemptsUsed: number; + maxAttemptsTotal: number; + exhausted: boolean; + lastAttemptOn: string | null; + exhaustedNotifiedOn: string | null; + alertDelivered: boolean | null; + alertError: string | null; + alertMode: RetryAlertConfig["alertMode"]; + overrideHandlerId: string | null; + overrideHandlerProperties: Record | null; + resolvedHandlerId: string | null; + resolvedHandlerProperties: Record | null; + resolvedFrom: RetryAlertLevel | null; + silencedAt: RetryAlertLevel | null; +} + +const toUsageRow = (r: RawUsageRow): RetryUsageRow => ({ + integrationId: r.subscriptionId, + integrationName: r.subscriptionName, + groupId: r.groupId, + groupName: r.groupName, + used: r.attemptsUsed, + total: r.maxAttemptsTotal, + exhausted: r.exhausted, + lastAttemptOn: r.lastAttemptOn, + resolvedHandlerId: r.resolvedHandlerId, + resolvedHandlerProperties: r.resolvedHandlerProperties ?? {}, + resolvedFrom: r.resolvedFrom, + silencedAt: r.silencedAt, + override: { + alertMode: r.alertMode ?? "Inherit", + alertHandlerId: r.overrideHandlerId, + alertHandlerProperties: r.overrideHandlerProperties ?? {}, + }, + // Only an alert that was actually raised has an outcome. Delivery is reported apart from + // the claim because the two can disagree, and the disagreement is the whole point. + alert: r.exhaustedNotifiedOn + ? { claimedOn: r.exhaustedNotifiedOn, delivered: r.alertDelivered, error: r.alertError } + : null, +}); + +interface RawAttempt { + xchangeId: string; + attemptNumber: number | null; + failedOn: string; + exception: string; + retryPending: boolean; + retryBlockedReason: string | null; +} + +export const retryPolicyMethods = { + async listRetryPolicies(): Promise { + const [res, subs] = await Promise.all([ + get>("/retrypolicies"), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByRetryPolicy = new Map(); + for (const s of subs.result ?? []) { + if (s.retryPolicyId == null) continue; + countByRetryPolicy.set(s.retryPolicyId, (countByRetryPolicy.get(s.retryPolicyId) ?? 0) + 1); + } + return (res.result ?? []).map((p) => ({ + id: p.id, + name: p.name, + groupCount: p.groupCount, + createdOn: "", + usedByCount: countByRetryPolicy.get(p.id) ?? 0, + })); + }, + + async searchRetryPolicies(query: { + search: string; + offset: number; + limit: number; + }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const [res, subs] = await Promise.all([ + get>(`/retrypolicies?${qs}`), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByRetryPolicy = new Map(); + for (const s of subs.result ?? []) { + if (s.retryPolicyId == null) continue; + countByRetryPolicy.set(s.retryPolicyId, (countByRetryPolicy.get(s.retryPolicyId) ?? 0) + 1); + } + return { + total: res.totalCount, + result: (res.result ?? []).map((p) => ({ + id: p.id, + name: p.name, + groupCount: p.groupCount, + createdOn: "", + usedByCount: countByRetryPolicy.get(p.id) ?? 0, + })), + }; + }, + + getRetryPolicy: fetchDetail, + + async createRetryPolicy({ name }: { name: string }): Promise { + const id = await post("/retrypolicies", { name, groups: [] }); + return { id, name, groups: [], createdOn: "", alertHandlerId: null, alertHandlerProperties: {} }; + }, + + async updateRetryPolicy( + id: number, + changes: { + name: string; + groups: RetryGroup[]; + alertHandlerId: string | null; + alertHandlerProperties: Record; + }, + ): Promise { + // Update replaces the whole policy, so every field it accepts has to be sent back. Omitting + // the alert cleared it on the server on every save — the settings were still on screen, and + // gone from the database. + await post(`/retrypolicies/${id}`, { + name: changes.name, + groups: changes.groups.map(toRawGroup), + alertHandlerId: changes.alertHandlerId, + alertHandlerProperties: changes.alertHandlerProperties, + }); + return { id, ...changes, createdOn: "" }; + }, + + async getRetryUsage(policyId: number): Promise { + const rows = await post(`/retrypolicies/${policyId}/usage`, {}); + return (rows ?? []).map(toUsageRow); + }, + + async getIntegrationRetryUsage(integrationId: number): Promise { + const rows = await post(`/subscriptions/${integrationId}/retryusage`, {}); + return (rows ?? []).map(toUsageRow); + }, + + async getRetryAttempts( + policyId: number, + pair: { integrationId: number; groupId: string }, + ): Promise { + const res = await post<{ total: number; attempts: RawAttempt[] }>( + `/retrypolicies/${policyId}/attempts`, + { subscriptionId: pair.integrationId, groupId: pair.groupId }, + ); + return { + total: res?.total ?? 0, + attempts: (res?.attempts ?? []).map((a) => ({ + exchangeId: a.xchangeId, + attemptNumber: a.attemptNumber, + failedOn: a.failedOn, + error: a.exception, + retryPending: a.retryPending, + blockedReason: a.retryBlockedReason, + })), + }; + }, + + async resetRetryUsage(policyId: number, pair?: { integrationId?: number; groupId?: string }): Promise { + await post(`/retrypolicies/${policyId}/resetusage`, { + subscriptionId: pair?.integrationId ?? null, + groupId: pair?.groupId ?? null, + }); + }, + + async resetIntegrationRetryUsage(integrationId: number, groupId?: string): Promise { + await post(`/subscriptions/${integrationId}/resetretryusage`, { groupId: groupId ?? null }); + }, + + async saveRetryAlertOverride( + policyId: number, + input: { integrationId: number; groupId: string } & RetryAlertConfig, + ): Promise { + await post(`/retrypolicies/${policyId}/savealertoverride`, { + subscriptionId: input.integrationId, + groupId: input.groupId, + alertMode: input.alertMode, + alertHandlerId: input.alertHandlerId, + alertHandlerProperties: input.alertHandlerProperties, + }); + }, + + async deleteRetryPolicy(id: number): Promise { + await request(`/retrypolicies/${id}`, { method: "DELETE" }); + }, + + async testRetryPolicy(input: { + groups: RetryGroup[]; + resultType: RetryResultType; + content: string; + attempts: number; + }): Promise { + const res = await post<{ attempts: RawTestAttempt[] }>("/retrypolicies/test", { + groups: input.groups.map(toRawGroup), + resultType: input.resultType, + content: input.content, + attemptsToSimulate: input.attempts, + }); + return (res.attempts ?? []).map((a) => ({ + attempt: a.attemptNumber, + shouldRetry: a.shouldRetry, + delaySeconds: a.delaySeconds ?? undefined, + matchedGroup: a.matchedGroupName ?? undefined, + reason: a.reason, + })); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts b/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts new file mode 100644 index 00000000..ffeba36a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts @@ -0,0 +1,38 @@ +/** + * Builds a Searchy query string — the `filter=Field:Rule:Value` + `page`/`size` + * convention every list endpoint in this API already speaks (see `buildExchangeQuery` + * for the hand-written original this generalizes). + * + * `page` and `size` are always sent together, never one without the other: at least + * one backend handler (`Subscriptions/Search.cs`, filtering by name) diverts a text + * filter through an in-memory `Skip(size * page).Take(size)` with no "size == 0 means + * unbounded" guard, so an omitted size silently returns zero rows despite a correct + * total count. Always supplying both sidesteps that regardless of which handler runs. + */ +export function buildListQuery(opts: { + /** [Field, Rule, Value] triples — Rule 1 = EqualsTo, 4 = Contains. Skipped when Value is "". */ + filters?: [string, number, string | number][]; + /** [Field, Order] — Order 1 = ascending, 2 = descending. */ + sort?: [string, number]; + offset: number; + limit: number; +}): string { + const params = new URLSearchParams(); + for (const [field, rule, value] of opts.filters ?? []) { + if (value !== "" && value !== undefined && value !== null) params.append("filter", `${field}:${rule}:${value}`); + } + if (opts.sort) params.append("sort", `${opts.sort[0]}:${opts.sort[1]}`); + params.set("page", String(Math.floor(opts.offset / opts.limit))); + params.set("size", String(opts.limit)); + return params.toString(); +} + +export const SEARCHY_RULE = { + equalsTo: 1, + contains: 4, +} as const; + +export const SEARCHY_SORT = { + asc: 1, + desc: 2, +} as const; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/session.ts b/SW.Bitween.Web/ClientApp/src/api/http/session.ts new file mode 100644 index 00000000..88076776 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/session.ts @@ -0,0 +1,116 @@ +import { ApiRequestError, type Session, type User } from "../types"; +import { getAppConfig } from "./appConfig"; +import { clearToken, get, getToken, post, setToken } from "./request"; + +/** GET /accounts/profile — camelCase ProfileModel. */ +interface Profile { + id: number; + email: string; + name: string; + /** Legacy coarse role, kept for older API clients. Authorization uses `permissions`. */ + role: string; + disabled: boolean; + createdOn: string; + roles: { id: number; name: string }[] | null; + permissions: string[] | null; +} + +/** POST /accounts/login → { jwt }. */ +interface LoginResult { + jwt: string; +} + +const buildSession = (profile: Profile): Session => { + const user: User = { + id: String(profile.id), + displayName: profile.name, + email: profile.email, + roleIds: (profile.roles ?? []).map((r) => String(r.id)), + status: profile.disabled ? "disabled" : "active", + // Always null here: you cannot be signed in and locked out at the same time. + lockedUntil: null, + microsoftLinked: false, + createdOn: profile.createdOn, + }; + return { + user, + roles: (profile.roles ?? []).map((r) => ({ id: String(r.id), name: r.name })), + // Resolved server-side from the user's roles, so a revoked role takes effect on next load. + permissions: profile.permissions ?? [], + }; +}; + +const loadSession = async (): Promise => buildSession(await get("/accounts/profile")); + +export const sessionMethods = { + async getSession(): Promise { + // No stored Jwt → anonymous; don't probe the backend (an expired token still + // gets refreshed via cookie inside request() on its 401). The token is only + // cleared on logout or an unrecoverable 401, so returning users keep it. + if (!getToken()) return null; + try { + return await loadSession(); + } catch { + // Token invalid and no refresh cookie → signed out. Show login, don't fake it. + return null; + } + }, + + async login(email: string, password: string): Promise { + const { jwt } = await post("/accounts/login", { + Username: email, + Password: password, + }); + setToken(jwt); + return loadSession(); + }, + + async loginWithMicrosoft(): Promise { + const cfg = await getAppConfig(); + if (!cfg.msalClientId) + throw new ApiRequestError("MS_NOT_CONFIGURED", "Microsoft sign-in isn't configured."); + + // Lazy so MSAL stays out of the initial bundle. + const { PublicClientApplication } = await import("@azure/msal-browser"); + const msal = new PublicClientApplication({ + auth: { + clientId: cfg.msalClientId, + ...(cfg.msalTenantId + ? { authority: `https://login.microsoftonline.com/${cfg.msalTenantId}` } + : {}), + }, + }); + await msal.initialize(); + const result = await msal.loginPopup({ + ...(cfg.msalRedirectUri ? { redirectUri: cfg.msalRedirectUri } : {}), + scopes: ["openid"], + }); + if (!result.idToken) + throw new ApiRequestError("MS_LOGIN_FAILED", "Microsoft didn't return a sign-in token."); + + const { jwt } = await post("/accounts/login", { MsToken: result.idToken }); + setToken(jwt); + return loadSession(); + }, + + async logout(): Promise { + try { + await post("/accounts/logout"); + } finally { + clearToken(); + } + }, + + async updateProfile(changes: { displayName: string }): Promise { + const current = await loadSession(); + await post(`/accounts/${current.user.id}`, { name: changes.displayName }); + return loadSession(); + }, + + async changePassword(currentPassword: string, newPassword: string): Promise { + await post("/accounts/changePassword", { + oldPassword: currentPassword, + newPassword, + }); + }, +}; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/settings.ts b/SW.Bitween.Web/ClientApp/src/api/http/settings.ts new file mode 100644 index 00000000..b41d1817 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/settings.ts @@ -0,0 +1,21 @@ +import type { ApiClient } from "../client"; +import type { SettingRow } from "../types"; +import { get, post, request } from "./request"; + +export const settingsMethods = { + /** + * The backend owns the catalog (label, section, kind, default, secret), so rows arrive + * ready to render — including which section each belongs to and, for secrets, only + * whether a value is set. + */ + listSettings(): Promise { + return get("/settings"); + }, + + /** A null value resets the setting, which is a DELETE rather than a write. */ + async updateSetting(key: string, value: string | null): Promise { + const path = `/settings/${encodeURIComponent(key)}`; + if (value === null) await request(path, { method: "DELETE" }); + else await post(path, { value }); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts b/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts new file mode 100644 index 00000000..a8feb501 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts @@ -0,0 +1,61 @@ +import type { InlineIntegrationDraft, Schedule } from "../types"; +import { toRawMatchExpression } from "./matchExpression"; + +/** + * The subscription wire shape, in the one place both the integration endpoints and + * the gateway endpoints can reach. + * + * It lives here rather than in `integrations.ts` because `gateways.ts` needs it too, + * and `integrations.ts` already imports `gateways.ts` — putting it there would make + * that cycle mutual. + */ + +export interface RawKeyAndValue { + key: string; + value: string; +} + +export interface RawSchedule { + recurrence: Schedule["recurrence"]; + days: number; + hours: number; + minutes: number; + backwards: boolean; +} + +export const toKvArray = (record: Record): RawKeyAndValue[] => + Object.entries(record).map(([key, value]) => ({ key, value })); + +export const toRawSchedules = (schedules: Schedule[]): RawSchedule[] => + schedules.map((s) => ({ + recurrence: s.recurrence, + days: s.days, + hours: s.hours, + minutes: s.minutes, + backwards: s.backwards, + })); + +/** + * An integration defined on a gateway's canvas, in the shape the gateway endpoints + * take. No `documentId`: a bus gateway imposes its own, and the API-gateway caller + * adds the one its picker chose. + */ +export const inlineIntegrationBody = (d: InlineIntegrationDraft) => ({ + name: d.name.trim(), + inactive: !d.enabled, + workGroupId: d.workGroupId, + retryPolicyId: d.retryPolicyId, + customRetryPolicy: null, + receiverId: d.receiverId, + receiverProperties: toKvArray(d.receiverProperties), + validatorId: d.validatorId, + validatorProperties: toKvArray(d.validatorProperties), + mapperId: d.mapperId, + mapperProperties: toKvArray(d.mapperProperties), + handlerId: d.handlerId, + handlerProperties: toKvArray(d.handlerProperties), + matchExpression: toRawMatchExpression(d.matchExpression), + schedules: toRawSchedules(d.schedules), + responseSubscriptionId: d.responseIntegrationId, + responseMessageTypeName: d.responseMessageTypeName, +}); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/team.ts b/SW.Bitween.Web/ClientApp/src/api/http/team.ts new file mode 100644 index 00000000..c00f919b --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/team.ts @@ -0,0 +1,167 @@ +import type { ApiClient } from "../client"; +import { + ApiRequestError, + type PermissionArea, + type PermissionKey, + type Role, + type User, +} from "../types"; +import { get, post, request } from "./request"; + +interface SearchyResponse { + result: T[]; + totalCount: number; +} + +interface RawRoleSummary { + id: number; + name: string; +} + +interface RawAccount { + id: number; + name: string; + email: string; + role: string; + disabled: boolean; + /** Set by the backend's failed-sign-in lockout; null or in the past means not locked. */ + lockoutEnd: string | null; + createdOn: string; + roles: RawRoleSummary[] | null; +} + +interface RawRole { + id: number; + name: string; + description: string | null; + isSystem: boolean; + permissions: string[] | null; + memberCount: number; + createdOn: string; +} + +const toUser = (r: RawAccount): User => ({ + id: String(r.id), + displayName: r.name, + email: r.email, + roleIds: (r.roles ?? []).map((role) => String(role.id)), + status: r.disabled ? "disabled" : "active", + // Past lockouts are not state anyone can act on, so they read as no lockout at all. + lockedUntil: r.lockoutEnd && new Date(r.lockoutEnd) > new Date() ? r.lockoutEnd : null, + // Not tracked by the backend: no login-method projection, no last-seen column. + microsoftLinked: false, + createdOn: r.createdOn, +}); + +const toRole = (r: RawRole): Role => ({ + id: String(r.id), + name: r.name, + description: r.description ?? "", + permissions: r.permissions ?? [], + isSystem: r.isSystem, + createdOn: r.createdOn, + memberCount: r.memberCount, +}); + +/** One page big enough for any realistic team — the backend defaults to 20. */ +const ALL_MEMBERS = 500; + +async function fetchUsers(): Promise { + const res = await get>(`/accounts?limit=${ALL_MEMBERS}`); + return (res.result ?? []).map(toUser); +} + +async function fetchUser(id: string): Promise { + // No GET /accounts/{id} exists, so read the member out of the list. + const user = (await fetchUsers()).find((u) => u.id === id); + if (!user) throw new ApiRequestError("NOT_FOUND", "This member no longer exists."); + return user; +} + +export const teamMethods = { + // — permission catalog — + async getPermissionCatalog(): Promise { + return await get("/permissions"); + }, + + // — members — + listUsers: fetchUsers, + getUser: fetchUser, + + async createUser({ + displayName, + email, + password, + roleIds, + }: { + displayName: string; + email: string; + password: string; + roleIds: string[]; + }): Promise { + const id = await post("/accounts", { + name: displayName, + email, + password, + roleIds: roleIds.map(Number), + }); + return fetchUser(String(id)); + }, + + async setUserPassword(id: string, password: string): Promise { + await post(`/accounts/${id}/setPassword`, { password }); + }, + + async updateUserRoles(id: string, roleIds: string[]): Promise { + await post(`/accounts/${id}/setRoles`, { roleIds: roleIds.map(Number) }); + return fetchUser(id); + }, + + /** Clears a failed-sign-in lockout early, rather than waiting it out. */ + async unlockUser(id: string): Promise { + await post(`/accounts/${id}/unlock`, {}); + return fetchUser(id); + }, + + async setUserDisabled(id: string, disabled: boolean): Promise { + await post(`/accounts/${id}/setDisabled`, { disabled }); + return fetchUser(id); + }, + + async deleteUser(id: string): Promise { + await post(`/accounts/${id}/remove`, {}); + }, + + // — roles — + async listRoles(): Promise { + const res = await get>("/roles?pageSize=200"); + return (res.result ?? []).map(toRole); + }, + + async getRole(id: string): Promise { + const raw = await get(`/roles/${id}`); + if (!raw) throw new ApiRequestError("NOT_FOUND", "This role no longer exists."); + return toRole(raw); + }, + + async createRole(input: { + name: string; + description: string; + permissions: PermissionKey[]; + }): Promise { + const id = await post("/roles", input); + return teamMethods.getRole(String(id)); + }, + + async updateRole( + id: string, + input: { name: string; description: string; permissions: PermissionKey[] }, + ): Promise { + await post(`/roles/${id}`, input); + return teamMethods.getRole(id); + }, + + async deleteRole(id: string): Promise { + await request(`/roles/${id}`, { method: "DELETE" }); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts b/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts new file mode 100644 index 00000000..51b9a303 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts @@ -0,0 +1,191 @@ +import type { ApiClient } from "../client"; +import { + ApiRequestError, + type IntegrationType, + type WorkGroup, + type WorkGroupDetail, + type WorkGroupRow, + type Paged, +} from "../types"; +import { get, getEnrichment, post } from "./request"; + +interface SearchyResponse { + result: T[]; + totalCount: number; +} +interface RawWorkGroup { + id: number; + name: string; + busMessageName: string; + options: { rabbitMqOptions: { prefetch: number | null; priority: number | null } | null } | null; + processorNodeCount: number | null; +} +interface RawSubscriptionRef { + id: number; + name: string; + type: number | string; +} + +const SUB_TYPE_BY_NUM: Record = { + 1: "Internal", + 2: "ApiCall", + 4: "Receiving", + 8: "Aggregation", + 16: "GatewayApiCall", + 32: "BusGateway", +}; +const INTEGRATION_TYPES: IntegrationType[] = [ + "Receiving", + "GatewayApiCall", + "BusGateway", + "Internal", + "ApiCall", + "Aggregation", +]; +/** Enums may arrive as the numeric value or the name in any case. */ +const toIntegrationType = (t: number | string): IntegrationType => { + if (typeof t === "number") return SUB_TYPE_BY_NUM[t] ?? "Internal"; + return INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; +}; + +async function fetchSubscriptionsByWorkGroup(workGroupId: number): Promise { + const res = await get>( + `/subscriptions?filter=${encodeURIComponent(`WorkGroupId:1:${workGroupId}`)}`, + ); + return res.result ?? []; +} + +// WorkGroup has no CreatedOn column on the backend. +const toWorkGroup = (w: RawWorkGroup): WorkGroup => ({ + id: w.id, + name: w.name, + busMessageName: w.busMessageName, + options: { + rabbitMqOptions: { + consumerSettings: { + prefetch: w.options?.rabbitMqOptions?.prefetch ?? 0, + priority: w.options?.rabbitMqOptions?.priority ?? 0, + }, + }, + }, + createdOn: "", +}); + +// The backend's own default kicks in only when the caller omits the param +// entirely (`request.Limit ??= 20`), so passing an explicit, generously large +// one is how "everything" is asked for — there is no separate "unbounded" +// value the way the shared Searchy endpoints have with size=0. +const EVERYTHING = 1_000_000; + +async function fetchRows(): Promise { + const res = await get>(`/workgroups?offset=0&limit=${EVERYTHING}`); + return res.result ?? []; +} + +async function fetchPagedRows(query: { + search: string; + offset: number; + limit: number; +}): Promise<{ rows: RawWorkGroup[]; total: number }> { + const params = new URLSearchParams({ offset: String(query.offset), limit: String(query.limit) }); + if (query.search.trim()) params.set("name", query.search.trim()); + const res = await get>(`/workgroups?${params.toString()}`); + return { rows: res.result ?? [], total: res.totalCount }; +} + +export const workGroupMethods = { + async listWorkGroups(): Promise { + const [rows, subs] = await Promise.all([ + fetchRows(), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByWorkGroup = new Map(); + for (const s of subs.result ?? []) { + if (s.workGroupId == null) continue; + countByWorkGroup.set(s.workGroupId, (countByWorkGroup.get(s.workGroupId) ?? 0) + 1); + } + return rows.map((w) => ({ + ...toWorkGroup(w), + usedByCount: countByWorkGroup.get(w.id) ?? 0, + consumerCount: w.processorNodeCount ?? 0, + })); + }, + + async searchWorkGroups(query: { + search: string; + offset: number; + limit: number; + }): Promise> { + const [{ rows, total }, subs] = await Promise.all([ + fetchPagedRows(query), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByWorkGroup = new Map(); + for (const s of subs.result ?? []) { + if (s.workGroupId == null) continue; + countByWorkGroup.set(s.workGroupId, (countByWorkGroup.get(s.workGroupId) ?? 0) + 1); + } + return { + total, + result: rows.map((w) => ({ + ...toWorkGroup(w), + usedByCount: countByWorkGroup.get(w.id) ?? 0, + consumerCount: w.processorNodeCount ?? 0, + })), + }; + }, + + async getWorkGroup(id: number): Promise { + const [rows, subs] = await Promise.all([fetchRows(), fetchSubscriptionsByWorkGroup(id)]); + const w = rows.find((x) => x.id === id); + if (!w) throw new ApiRequestError("NOT_FOUND", "This work group no longer exists."); + return { + ...toWorkGroup(w), + integrations: subs.map((s) => ({ id: s.id, name: s.name, type: toIntegrationType(s.type) })), + }; + }, + + async createWorkGroup(input: { + name: string; + busMessageName: string; + prefetch: number; + priority: number; + }): Promise { + const created = await post<{ id: number }>("/workgroups", { + name: input.name, + busMessageName: input.busMessageName, + options: { rabbitMqOptions: { prefetch: input.prefetch, priority: input.priority } }, + }); + return { + id: created.id, + name: input.name, + busMessageName: input.busMessageName, + options: { rabbitMqOptions: { consumerSettings: { prefetch: input.prefetch, priority: input.priority } } }, + createdOn: "", + }; + }, + + async updateWorkGroup( + id: number, + changes: { name: string; busMessageName: string; prefetch: number; priority: number }, + ): Promise { + await post(`/workgroups/${id}`, { + name: changes.name, + busMessageName: changes.busMessageName, + options: { rabbitMqOptions: { prefetch: changes.prefetch, priority: changes.priority } }, + }); + return { + id, + name: changes.name, + busMessageName: changes.busMessageName, + options: { + rabbitMqOptions: { consumerSettings: { prefetch: changes.prefetch, priority: changes.priority } }, + }, + createdOn: "", + }; + }, + + async deleteWorkGroup(id: number): Promise { + await post(`/workgroups/${id}/delete`, {}); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/index.ts b/SW.Bitween.Web/ClientApp/src/api/index.ts new file mode 100644 index 00000000..31dadb7d --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/index.ts @@ -0,0 +1,15 @@ +import type { ApiClient } from "./client"; +import { httpClient } from "./http/httpClient"; + +/** + * The swap point. Everything in the UI imports `api` from here. + * This is the real HTTP client — no mock, no toggle. Domains not yet wired + * reject with NotWiredError so their screens read as honestly-not-connected + * (see BACKEND_WIRING_PLAN §2). + */ +export const api: ApiClient = httpClient; + +export { getAppConfig, resetAppConfig } from "./http/appConfig"; +export type { AppConfig } from "./http/appConfig"; +export { referencesGlobal, referencesPartnerProp } from "./http/references"; +export * from "./types"; diff --git a/SW.Bitween.Web/ClientApp/src/api/permissions.ts b/SW.Bitween.Web/ClientApp/src/api/permissions.ts new file mode 100644 index 00000000..a1cb5923 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/permissions.ts @@ -0,0 +1,48 @@ +import { useQuery } from "@tanstack/react-query"; +import { api } from "."; +import type { ActionId, PermissionArea, PermissionKey } from "./types"; + +/** + * The permission catalog is defined and enforced in the backend (SW.Bitween.Sdk/Model/Permissions.cs) + * and served by GET /permissions. It deliberately does not live here as well: a second copy would + * silently drift, and the role matrix would start offering grants the API ignores. + * + * What stays here is presentation — how an action is worded and the order columns appear in. + */ +export const ACTION_LABELS: Record = { + view: "View", + create: "Create", + edit: "Edit", + delete: "Delete", + operate: "Operate", +}; + +/** All actions in the order matrix columns render. */ +export const ACTION_ORDER: ActionId[] = ["view", "create", "edit", "delete", "operate"]; + +export const permissionKey = (areaId: string, actionId: ActionId): PermissionKey => + `${areaId}.${actionId}`; + +/** Static per deployment, so it's fetched once and kept. */ +export function usePermissionCatalog() { + return useQuery({ + queryKey: ["permission-catalog"], + queryFn: () => api.getPermissionCatalog(), + staleTime: Infinity, + }); +} + +export const allKeysIn = (areas: PermissionArea[]): PermissionKey[] => + areas.flatMap((area) => area.actions.map((a) => permissionKey(area.id, a.id as ActionId))); + +/** Navigation groups, in the order the backend lists them. */ +export const groupsIn = (areas: PermissionArea[]): string[] => [ + ...new Set(areas.map((area) => area.group)), +]; + +/** "Integrations · Edit" for a key, falling back to the raw key for anything unrecognised. */ +export const labelIn = (areas: PermissionArea[], key: PermissionKey): string => { + const [areaId, actionId] = key.split("."); + const area = areas.find((a) => a.id === areaId); + return area ? `${area.label} · ${ACTION_LABELS[actionId as ActionId] ?? actionId}` : key; +}; diff --git a/SW.Bitween.Web/ClientApp/src/api/types.ts b/SW.Bitween.Web/ClientApp/src/api/types.ts new file mode 100644 index 00000000..4e35d18c --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/types.ts @@ -0,0 +1,1002 @@ +/** Shared API shapes. These model the contract the real backend will need to satisfy. */ + +export type ActionId = "view" | "create" | "edit" | "delete" | "operate"; + +/** e.g. "subscriptions.edit" */ +export type PermissionKey = string; + +export interface PermissionAction { + id: ActionId; + /** What this specific grant allows, in end-user words. */ + description: string; +} + +export interface PermissionArea { + id: string; + label: string; + group: string; + description: string; + actions: PermissionAction[]; +} + +export interface Role { + id: string; + name: string; + description: string; + permissions: PermissionKey[]; + /** Built-in roles (Administrator) can't be edited or deleted. */ + isSystem: boolean; + createdOn: string; + memberCount: number; +} + +export type UserStatus = "active" | "disabled"; + +export interface User { + id: string; + displayName: string; + email: string; + roleIds: string[]; + status: UserStatus; + /** + * Set while the account is locked out after repeated failed sign-ins. Orthogonal to + * `status`: a lockout is automatic and expires on its own, where disabling is an + * admin decision that does not. Null once it has passed. + */ + lockedUntil: string | null; + /** Whether a Microsoft account is linked for SSO. */ + microsoftLinked: boolean; + createdOn: string; + lastActiveOn?: string; +} + +/** Just enough to name a role. Full definitions need the roles.view permission. */ +export interface RoleSummary { + id: string; + name: string; +} + +export interface Session { + user: User; + roles: RoleSummary[]; + /** The union of every permission the user's roles grant — resolved by the backend. */ + permissions: PermissionKey[]; +} + +export interface ApiError { + code: string; + message: string; +} + +export class ApiRequestError extends Error { + code: string; + constructor(code: string, message: string) { + super(message); + this.code = code; + } +} + +/** + * Thrown by any ApiClient method whose domain hasn't been wired to the real + * backend yet. Screens surface this as an honest "Not connected yet" state — + * never fake data. Batches remove these as they land. + */ +export class NotWiredError extends Error { + code = "NOT_WIRED"; + constructor(method: string) { + super(`"${method}" isn't connected to the backend yet.`); + } +} + +// ——— Configuration entities (sub-phase 2) ——— + +/** Lightweight references for "used by" panels. */ +export interface IntegrationSetupRef { + id: number; + name: string; + type: IntegrationType; +} +export interface ApiGatewayAttachmentRef { + gatewayId: number; + gatewayName: string; + urlName: string; +} +export interface BusGatewayRouteRef { + gatewayId: number; + gatewayName: string; + matchExpression: string; +} +/** The pipeline stages that can each produce a document for an exchange. */ +export type ExchangeDocStage = "Input" | "Mapped" | "Handled"; +export interface ExchangeDocument { + stage: ExchangeDocStage; + content: string; +} +export interface ExchangeRef { + id: string; + partnerName?: string; + informationTypeCode: string; + status: ExchangeStatus; + on: string; + /** What the exchange was, in the information type's own terms. The lead column. */ + promotedProperties?: Record | null; + /** Documents produced as the exchange moved through the pipeline, for drill-down previews. */ + documents?: ExchangeDocument[]; +} + +/** + * Lightweight summary of every integration, cached client-side so pages + * can answer "who uses this property/value/policy?" without new requests. + * Derived server-side by scanning adapter property values for tokens. + */ +export interface IntegrationInfo { + id: number; + name: string; + type: IntegrationType; + /** + * The integration's OWN partner, which only the legacy types carry. Partners + * linked through a gateway attachment or a bus route are NOT here — the list + * endpoint doesn't know about them. Use `usePartnerIntegrations()` when you + * need the full picture. + */ + partnerIds: number[]; + informationTypeId: number; + workGroupId: number | null; + retryPolicyId: number | null; + /** + * Its delivery step. Null means nothing is delivered — and since a response is + * whatever the delivery hands back, null here means the two response fields below + * can never be reached, however they are set. + */ + handlerId: string | null; + /** The bus message its delivery response is published as, if any. */ + responseMessageTypeName: string | null; + /** The integration its delivery response is handed straight to, if any. */ + responseIntegrationId: number | null; + /** + * Reference tokens found in its adapter properties. Both are matched + * case-insensitively, as the backend resolver does — compare them with + * `referencesPartnerProp`/`referencesGlobal` rather than `===`. + */ + partnerPropKeys: string[]; + globals: { setId: string; keys: string[] }[]; +} +export interface TrailEntry { + on: string; + action: "Created" | "Updated"; + by: string; + /** Absent for system-attributed entries with no real team member behind them. */ + byUserId?: string; +} + +export interface ApiCredentialRef { + name: string; + /** Only the first characters — full keys are shown once, at creation. */ + keyPrefix: string; + createdOn: string; +} + +export interface Partner { + id: number; + name: string; + /** Referenced in adapter configs as {{partner.KEY}}. */ + adapterProperties: Record; + /** The built-in SYSTEM partner can't be renamed or deleted. */ + isSystem: boolean; + createdOn: string; +} +export interface PartnerRow extends Partner { + credentialCount: number; + usedByCount: number; + /** Property names only — the list endpoint never sends values, which may be secrets. */ + propertyKeys: string[]; +} +export interface PartnerDetail extends Partner { + apiCredentials: ApiCredentialRef[]; + apiGateways: ApiGatewayAttachmentRef[]; + busGatewayRoutes: BusGatewayRouteRef[]; + recentExchanges: ExchangeRef[]; +} + +export type InformationTypeFormat = "Json" | "Xml"; + +export interface InformationType { + id: number; + /** Short unique identity shown across the system, e.g. PURCHASE_ORDER. Optional. */ + code?: string; + name: string; + format: InformationTypeFormat; + busEnabled: boolean; + busMessageTypeName?: string; + /** How long an identical incoming payload counts as a duplicate. 0 = off. */ + duplicateIntervalMinutes: number; + disregardsUnfilteredMessages: boolean; + /** Friendly name → JSONPath/XPath, matched by routes and filters. */ + promotedProperties: { key: string; path: string }[]; + createdOn: string; +} +export interface InformationTypeRow extends InformationType { + usedByCount: number; +} +export interface InformationTypeDetail extends InformationType { + integrationSetups: IntegrationSetupRef[]; + busGateways: { gatewayId: number; gatewayName: string }[]; + trail: TrailEntry[]; + recentExchanges: ExchangeRef[]; +} + +export interface GlobalValuesSet { + /** Caller-chosen slug; referenced as {{globals..}}. */ + id: string; + name: string; + values: Record; + createdOn: string; +} +/** Alias the ported mapper code types its global-set props with. */ +export type GlobalValuesSetRow = GlobalValuesSet; +export interface ValueSetUsage { + integrationSetup: IntegrationSetupRef; + keys: string[]; +} +export interface GlobalValuesSetDetail extends GlobalValuesSet { + usedBy: ValueSetUsage[]; +} + +// ——— Retry policies ——— + +export type RetryResultType = "Error" | "BadResult"; + +export type RetryMatcher = + | { type: "contains"; value: string; caseSensitive: boolean } + | { type: "regex"; pattern: string; flags: string } + | { type: "exceptionType"; value: string; includeInner: boolean } + | { type: "jsonPath"; path: string; op: "Eq" | "Neq" | "Contains" | "Exists" | "NotExists"; value?: string }; + +export type RetryDelay = + | { type: "fixed"; delaySeconds: number } + | { type: "linear"; initialSeconds: number; incrementSeconds: number } + | { type: "exponential"; initialSeconds: number; multiplier: number; maxSeconds: number }; + +/** + * Whether a level of the alert hierarchy names its own destination or defers upward. + * + * Resolved most-specific-first per integration and group: the pair's own override, then + * the group, then the policy. A level that sends **replaces** the one above rather than + * merging with it, so whichever level wins has to carry the handler and every property + * it needs. + */ +export type RetryAlertMode = "Inherit" | "Send" | "Silent"; + +/** Which level of that hierarchy decided, so a wrong destination can be traced to its source. */ +export type RetryAlertLevel = "SubscriptionGroup" | "Group" | "Policy"; + +/** A destination for budget-exhausted alerts, as configured at one level. */ +export interface RetryAlertConfig { + alertMode: RetryAlertMode; + alertHandlerId: string | null; + alertHandlerProperties: Record; +} + +export interface RetryGroup { + id: string; + name: string; + /** Lower runs first; the first matching group decides. */ + priority: number; + enabled: boolean; + appliesTo: RetryResultType[]; + /** OR logic; empty = any failure of the applicable kind. */ + matchers: RetryMatcher[]; + action: "Allow" | "Block"; + budget?: { maxAttemptsPerError: number; maxAttemptsTotal: number; delay: RetryDelay }; + notes?: string; + /** Where this group's budget-exhausted alert goes, for every integration using the policy. */ + alertMode: RetryAlertMode; + alertHandlerId: string | null; + alertHandlerProperties: Record; +} + +export interface RetryPolicy { + id: number; + name: string; + groups: RetryGroup[]; + createdOn: string; + /** The policy-wide alert destination, inherited by every group that doesn't name its own. */ + alertHandlerId: string | null; + alertHandlerProperties: Record; +} +export interface RetryPolicyListRow { + id: number; + name: string; + /** The list only counts groups; the full list is in the detail response. */ + groupCount: number; + createdOn: string; + usedByCount: number; +} +export interface RetryPolicyDetail extends RetryPolicy { + integrations: IntegrationSetupRef[]; +} + +/** + * What became of a budget-exhausted alert. + * + * `claimedOn` is when the alert was raised — all the counter itself records. Whether it then + * reached anyone is a separate fact that can fail, so the two are reported apart: a page showing + * only the claim tells the reader someone was notified when nobody was. + */ +export interface RetryAlertOutcome { + claimedOn: string; + /** Null when the alert was claimed but no delivery attempt was ever recorded. */ + delivered: boolean | null; + /** Why delivery failed, when it did. */ + error: string | null; +} + +/** + * The whole state of one integration-and-group pair: how much of the group's budget that + * integration has spent, and where the pair's budget-exhausted alert would go. + * + * Budgets are counted per pair — a shared policy gives every integration its own separate total + * — so there is no such thing as "this policy's usage". Any single figure on a policy or a group + * would be an aggregate matching nothing anyone can act on, which is why the pair is also what + * resetting and overriding both address. + */ +export interface RetryUsageRow { + integrationId: number; + integrationName: string; + groupId: string; + groupName: string; + used: number; + total: number; + /** Spent out: this integration gets no further automatic retries from this group. */ + exhausted: boolean; + /** Null when the pair has never failed — also how you know there is no counter to reset. */ + lastAttemptOn: string | null; + /** Where the alert actually goes, or null when nothing sends for this pair. */ + resolvedHandlerId: string | null; + resolvedHandlerProperties: Record; + resolvedFrom: RetryAlertLevel | null; + /** Which level deliberately switched the alert off — a decision, as against an oversight. */ + silencedAt: RetryAlertLevel | null; + /** This pair's own override; `Inherit` when it has none. */ + override: RetryAlertConfig; + alert: RetryAlertOutcome | null; +} + +/** One failure a group caught — what a usage row spent its budget on. */ +export interface RetryAttempt { + exchangeId: string; + /** How deep the retry chain was, 0 being the original delivery. Null for older failures. */ + attemptNumber: number | null; + failedOn: string; + error: string; + /** True while another attempt is still scheduled: the one thing here that is not history. */ + retryPending: boolean; + /** Why no further attempt was scheduled, when the policy refused one. */ + blockedReason: string | null; +} + +export interface RetryAttempts { + /** + * Every failure this group has caught for this integration. Failures outlive the counter, + * which is reset, so this is not the counter's value. + */ + total: number; + attempts: RetryAttempt[]; +} + +export interface RetryTestAttempt { + attempt: number; + shouldRetry: boolean; + delaySeconds?: number; + matchedGroup?: string; + reason: string; +} + +// ——— Notifiers ——— + +export interface Notifier { + id: number; + name: string; + /** Off pauses the notifier without losing its setup. */ + enabled: boolean; + /** Which exchange outcomes trigger a notification. */ + onFailed: boolean; + onBadResult: boolean; + onSuccess: boolean; + channelId: string; + channelProperties: Record; + /** Integrations this notifier watches; empty = it never fires. */ + integrationIds: number[]; + createdOn: string; +} + +/** One delivery attempt from the notification history. */ +export interface NotificationEntry { + xchangeId: string; + success: boolean; + exception?: string; + on: string; +} + +export interface NotifierDetail extends Notifier { + recentNotifications: NotificationEntry[]; +} + +// ——— Integrations (subscriptions) ——— + +/** + * Backend Subscription.Type. Aggregation exists in data but is deferred in + * this UI; Internal and ApiCall are legacy — shown and editable, never created. + */ +/** + * The editable fields of an integration being defined inline, while whatever points + * at it is being made. Mirrors the studio's own draft — deliberately, so the canvas + * can hand its draft straight to the client. + */ +export interface InlineIntegrationDraft { + name: string; + enabled: boolean; + workGroupId: number | null; + retryPolicyId: number | null; + receiverId: string | null; + receiverProperties: Record; + validatorId: string | null; + validatorProperties: Record; + mapperId: string | null; + mapperProperties: Record; + handlerId: string | null; + handlerProperties: Record; + matchExpression: MatchGroup | null; + schedules: Schedule[]; + responseIntegrationId: number | null; + responseMessageTypeName: string | null; +} + +export type IntegrationType = + | "Receiving" + | "GatewayApiCall" + | "BusGateway" + | "Internal" + | "ApiCall" + | "Aggregation"; + +export type AdapterKind = "receiver" | "handler" | "mapper" | "validator"; + +/** One configurable property of an adapter, from its startup-value metadata. */ +export interface AdapterProp { + key: string; + optional: boolean; + default?: string; + /** Secret values are write-only: masked after save, replaced not edited. */ + secret: boolean; + description?: string; +} + +export interface AdapterInfo { + id: string; + kind: AdapterKind; + /** Friendly display name, e.g. "HTTP endpoint" for NativeHttpHandler. */ + label: string; + /** Native adapters run in-process; others are deployed packages with versions. */ + native: boolean; + versions: string[]; + props: AdapterProp[]; +} + +/** + * Message filter over an information type's promoted properties. + * Groups are n-ary here (friendlier to edit); the backend's binary + * and/or tree converts losslessly both ways. + */ +export type MatchCondition = { + op: "oneOf" | "notOneOf"; + path: string; + values: string[]; +}; +export type MatchGroup = { + op: "and" | "or"; + children: MatchNode[]; +}; +export type MatchNode = MatchGroup | MatchCondition; + +export type Recurrence = "Hourly" | "Daily" | "Weekly" | "Monthly"; + +export interface Schedule { + recurrence: Recurrence; + /** Weekly: weekday 0–6 (Sun–Sat); Monthly: day of month 1–27; otherwise 0. */ + days: number; + hours: number; + minutes: number; + /** Count the offset from the end of the period instead of the start. */ + backwards: boolean; +} + +export interface Integration { + id: number; + name: string; + type: IntegrationType; + informationTypeId: number; + /** Direct partner — legacy Internal/ApiCall (and Aggregation) only. */ + partnerId: number | null; + /** Inverse of backend Inactive. Disabled = not scheduled, not matched. */ + enabled: boolean; + /** Paused still accepts work but holds it for later release. */ + pausedOn: string | null; + workGroupId: number | null; + retryPolicyId: number | null; + receiverId: string | null; + receiverProperties: Record; + validatorId: string | null; + validatorProperties: Record; + mapperId: string | null; + mapperProperties: Record; + handlerId: string | null; + handlerProperties: Record; + /** Legacy Internal only: which documents this integration picks up. */ + matchExpression: MatchGroup | null; + /** Receiving (and Aggregation) only. */ + schedules: Schedule[]; + /** Feed the handler's response into another integration. */ + responseIntegrationId: number | null; + responseMessageTypeName: string | null; + aggregationForId: number | null; + // — health (read-only) — + isRunning: boolean; + /** Receiving (and Aggregation) only — when the schedule will next fire, not when it last did. */ + nextReceiveOn: string | null; + consecutiveFailures: number; + lastException: string | null; + createdOn: string; +} + +export interface IntegrationRow { + id: number; + name: string; + type: IntegrationType; + informationTypeId: number; + informationTypeCode: string; + partners: { id: number; name: string }[]; + enabled: boolean; + paused: boolean; + isRunning: boolean; + consecutiveFailures: number; + lastException: string | null; + scheduleSummary?: string; + /** Receiving (and Aggregation) only — when the schedule will next fire, not when it last did. */ + nextReceiveOn: string | null; + createdOn: string; +} + +/** + * One execution of a scheduled integration, from the scheduler's own history. + * Kept for `RetentionDays` (~30) — older runs are purged, not archived here. + */ +export interface IntegrationRun { + startedOn: string; + endedOn: string | null; + durationMs: number | null; + /** Null while the run is still in progress. */ + success: boolean | null; + error: string | null; + node: string; + /** Someone pressed Receive now rather than waiting for the schedule. */ + manual: boolean; +} + +export interface IntegrationLastRun extends IntegrationRun { + integrationId: number; + /** Finished runs in the recent window; in-progress runs count as neither pass nor fail. */ + recentTotal: number; + recentSucceeded: number; +} + +/** One poll of a Receiving integration's own receive step — independent of the scheduler's + * run history, which only knows whether the method threw (it never does; failures here are + * caught and reported this way instead). */ +export type ReceiveOutcome = "Failed" | "NoNewData" | "Received"; + +export interface ReceiveAttemptExchange { + id: string; + status: ExchangeStatus; + promotedProperties: Record | null; +} + +export interface ReceiveAttemptRow { + id: number; + startedOn: string; + finishedOn: string; + outcome: ReceiveOutcome; + errorMessage: string | null; + exchanges: ReceiveAttemptExchange[]; +} + +/** + * Whether a scheduled integration will actually fire, straight from the scheduler. + * Everything here can disagree with what the integration's own record says, and + * when it does the job is silently dead rather than visibly broken. + */ +export interface ScheduleHealth { + integrationId: number; + scheduleCount: number; + /** Fewer than `scheduleCount` means a schedule exists that nothing will ever fire. */ + triggerCount: number; + state: "Normal" | "Paused" | "Blocked" | "Error" | "Complete" | "Missing"; + /** The scheduler's own next fire time, computed independently of `nextReceiveOn`. */ + nextFireOn: string | null; + /** Flagged as running with nothing executing — the concurrency guard is skipping every run. */ + stuck: boolean; +} + +export interface IntegrationDetail extends Integration { + informationTypeCode: string; + informationTypeName: string; + /** Where this integration is plugged in (entry points). */ + apiGatewayAttachments: { gatewayId: number; gatewayName: string; urlName: string; partnerId: number; partnerName: string }[]; + busGatewayRoutes: { gatewayId: number; gatewayName: string; partnerId: number | null; partnerName: string | null }[]; + watchingNotifiers: { id: number; name: string }[]; + recentExchanges: ExchangeRef[]; + trail: TrailEntry[]; +} + +export interface WorkGroupOptions { + rabbitMqOptions: { + consumerSettings: { + prefetch: number; + priority: number; + }; + }; +} + +export interface WorkGroup { + id: number; + name: string; + /** Queue name suffix; combined with the id to form the real queue name. */ + busMessageName: string; + options: WorkGroupOptions; + createdOn: string; +} +export interface WorkGroupRow extends WorkGroup { + usedByCount: number; + /** Best-effort live count from the RabbitMQ management API. */ + consumerCount: number; +} +export interface WorkGroupDetail extends WorkGroup { + integrations: IntegrationSetupRef[]; +} + +// ——— API gateways ——— + +export interface ApiGatewayAttachment { + partnerId: number; + partnerName: string; + integrationId: number; + integrationName: string; +} +export interface ApiGateway { + id: number; + name: string; + urlName: string; + /** Off but kept, with its attachments. Partners calling it get a 503. */ + inactive: boolean; + createdOn: string; +} +export interface ApiGatewayRow extends ApiGateway { + partnerCount: number; + attachments: ApiGatewayAttachment[]; +} +export interface ApiGatewayDetail extends ApiGateway { + attachments: ApiGatewayAttachment[]; +} + +// ——— Bus gateways ——— + +export interface BusGatewayRoute { + id: number; + integrationId: number; + integrationName: string; + partnerId: number | null; + partnerName: string | null; + /** null = route matches every message of the gateway's type. */ + matchExpression: MatchGroup | null; +} +export interface BusGateway { + id: number; + name: string; + informationTypeId: number; + /** Off but kept, with its routes. The message stops being offered to them. */ + inactive: boolean; + createdOn: string; +} +export interface BusGatewayRow extends BusGateway { + informationTypeCode: string; + routeCount: number; + routes: BusGatewayRoute[]; +} +export interface BusGatewayDetail extends BusGateway { + informationTypeCode: string; + informationTypeName: string; + routes: BusGatewayRoute[]; +} + +// ——— Settings ——— + +export type SettingValueKind = "string" | "number" | "boolean" | "color"; + +/** + * How a row behaves: + * - `editable` — stored in the database, changeable here, takes effect immediately. + * - `readonly` — an environment value, shown so you can see what this instance runs + * on. Read once at startup, so there's nothing to change here. + * - `presence` — an environment value whose content stays on the server; only + * whether it's set is reported. + */ +export type SettingAccess = "editable" | "readonly" | "presence"; + +/** + * One setting on the settings page. Only settings that take effect immediately are + * editable, so there is no "restart required" state to represent — anything read + * once at startup appears as `readonly` or `presence` instead. + */ +export interface SettingRow { + /** Stable key; mirrors the config path (e.g. "Bitween.RebexLicenseKey"). */ + key: string; + /** Grouping label owned by the backend catalog; the page derives its pills from these. */ + section: string; + label: string; + description: string; + kind: SettingValueKind; + /** The product default a reset returns to. Stored as a string per `kind`; empty for secrets. */ + defaultValue: string; + /** The stored value, or null for a secret — those never leave the server. */ + value: string | null; + secret: boolean; + /** The stored value differs from the product default, so a reset would do something. */ + overridden: boolean; + /** Whether the effective value is non-empty — the only way a secret reveals it is set. */ + hasValue: boolean; + /** + * Whether this row can be written. False for every environment value, and for a secret on an + * instance with no encryption key configured — there's nowhere safe to store that one. + */ + editable: boolean; + access: SettingAccess; +} + +// ——— Exchanges ——— + +/** + * The four observable outcomes of an exchange. "badResponse" = the handler + * delivered but the receiving system answered with a business-level error. + */ +export type ExchangeStatus = "processing" | "success" | "badResponse" | "failed"; + +export interface ExchangeFileRef { + name: string; + /** Bytes. */ + size: number; + /** Storage key to fetch this stage's content through `getExchangeDocument`; null if no file exists. */ + key: string | null; +} + +/** One exchange as the Exchanges page sees it — names pre-resolved for display. */ +export interface ExchangeRow { + id: string; + status: ExchangeStatus; + integrationId: number | null; + integrationName: string | null; + informationTypeId: number; + informationTypeCode: string; + partnerId: number | null; + partnerName: string | null; + startedOn: string; + /** null while still processing. */ + finishedOn: string | null; + correlationId: string | null; + /** Set when this exchange is a retry of another one. */ + retryFor: string | null; + /** Set when this exchange was rolled up into an aggregation exchange. */ + aggregationXchangeId: string | null; + /** A pending auto-retry, when the retry policy scheduled one. */ + scheduledRetryOn: string | null; + exception: string | null; + promotedProperties: Record | null; + /** True when the integration has no mapper — the Mapped stage is skipped. */ + mapperSkipped: boolean; + files: { + input: ExchangeFileRef | null; + mapped: ExchangeFileRef | null; + handled: ExchangeFileRef | null; + }; +} + +export interface ExchangeQuery { + status?: ExchangeStatus; + integrationId?: number; + partnerId?: number; + informationTypeId?: number; + /** Comma/pipe/newline separated; matches id, retryFor OR aggregationXchangeId. */ + ids?: string; + correlationId?: string; + /** Substring match against promoted property keys and values. */ + property?: string; + /** + * Narrows `property` to one promoted key. Set on its own it asks "has this key at + * all", which is worth being able to ask. + */ + propertyKey?: string; + from?: string; + to?: string; + offset: number; + limit: number; +} + +export interface Paged { + result: T[]; + total: number; +} + +// ——— Scheduled retries ——— + +/** A failed exchange whose retry policy scheduled an automatic retry. */ +export interface ScheduledRetryRow { + /** The exchange the retry will re-run. */ + id: string; + /** When the retry job will pick it up. */ + on: string; + integrationId: number | null; + integrationName: string | null; + informationTypeId: number; + informationTypeCode: string; + exception: string | null; + /** When the failed exchange originally started. */ + startedOn: string; + /** What the exchange carries — how a pending retry identifies itself in a list. */ + promotedProperties: Record | null; + /** + * The shared retry policy the integration currently points at. Null when the + * policy is defined inline on the integration instead, so the integration — not + * the Retry policies list — is where to go and look. + */ + retryPolicyId: number | null; + retryPolicyName: string | null; +} + +export interface ScheduledRetryQuery { + integrationId?: number; + informationTypeId?: number; + /** Substring match against the exception text. */ + exception?: string; + from?: string; + to?: string; + offset: number; + limit: number; +} + +// ——— Queue health (Ops) ——— + +export type QueueSeverity = "healthy" | "warning" | "critical"; + +export interface QueueHealthSummary { + totalConsumers: number; + unhealthyConsumers: number; + disconnectedConsumers: number; + totalQueueDepth: number; + totalRetryBacklog: number; + totalDeadLetterBacklog: number; + /** Messages per second, across all queues. */ + totalIncomingRate: number; + totalAckRate: number; + lastUpdated: string; +} + +/** + * What a queue is for. Resolved by `Ops/LaneResolver`, which sits next to + * `WorkGroup.GetBusMessageName()` — the formula that builds the name in the + * first place. The wording for each lane is this app's, in `QueueHealthPage`. + */ +export type QueueLane = "FrontDoor" | "Work" | "Notifications" | "Legacy" | "Control"; + +export interface ConsumerHealth { + name: string; + messageName: string; + queueName: string; + lane: QueueLane; + /** The name of the thing this lane belongs to; the raw message name if it no longer resolves. */ + title: string; + /** Set when the lane belongs to a work group, for drill-down. */ + workGroupId: number | null; + /** Set on a front door, for drill-down to the information type it listens for. */ + informationTypeId: number | null; + totalNodes: number; + processingCount: number; + queueCount: number; + retryCount: number; + failedCount: number; + priority: number; + prefetch: number; + incomingRate: number; + ackRate: number; + isBackpressured: boolean; + health: QueueSeverity; +} + +export interface RetryBacklogRow { + consumerName: string; + /** The lane's operator-facing name, resolved from the consumer rows. */ + title: string; + queueName: string; + retryBacklog: number; + incomingRate: number; + ackRate: number; + severity: QueueSeverity; +} + +export interface DeadLetterRow { + consumerName: string; + title: string; + queueName: string; + count: number; + lastExceptionType: string | null; + lastExceptionMessage: string | null; + lastFailedAt: string | null; +} + +/** + * A queue RabbitMQ has that nothing here reads. Deleting or renaming a work group + * leaves its queues behind, and every other view on this page is built from what the + * running process declares — so these are invisible everywhere else. + */ +export interface UnattendedQueue { + queueName: string; + messages: number; + retryMessages: number; + deadMessages: number; + /** Main plus whichever of its retry/dead queues still exist. */ + queues: number; +} + +export interface QueueAlert { + severity: "warning" | "critical"; + title: string; + detail: string; + queueName: string; + on: string; +} + +/** One poll = one snapshot; everything the Queue health page shows. */ +export interface QueueHealthSnapshot { + summary: QueueHealthSummary; + consumers: ConsumerHealth[]; + retryBacklog: RetryBacklogRow[]; + deadLetters: DeadLetterRow[]; + unattended: UnattendedQueue[]; + alerts: QueueAlert[]; +} + +// ——— Dashboard ——— + +export interface DashboardData { + today: { total: number; failed: number; processing: number }; + yesterdayTotal: number; + /** Percentage 0–100 across the last 7 days of finished exchanges. */ + successRate7d: number; + pendingRetries: number; + queueAlerts: number; + /** Last 14 days, oldest first; today is the final entry. */ + trafficByDay: { date: string; success: number; failed: number }[]; + /** Top integrations by 7-day traffic, busiest first. */ + busiest: { id: number; name: string; count: number; failed: number }[]; + latestFailures: { + id: string; + status: ExchangeStatus; + integrationId: number | null; + integrationName: string | null; + informationTypeCode: string; + on: string; + exception: string | null; + }[]; + attention: { + failingIntegrations: { id: number; name: string; consecutiveFailures: number }[]; + pausedIntegrations: { id: number; name: string }[]; + }; +} diff --git a/SW.Bitween.Web/ClientApp/src/auth/SessionContext.tsx b/SW.Bitween.Web/ClientApp/src/auth/SessionContext.tsx new file mode 100644 index 00000000..7c520dd7 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/auth/SessionContext.tsx @@ -0,0 +1,96 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { api, type PermissionKey, type Session } from "../api"; + +interface SessionContextValue { + session: Session | null; + /** True until the stored session has been checked once at startup. */ + initializing: boolean; + can: (permission: PermissionKey) => boolean; + signIn: (email: string, password: string) => Promise; + signInWithMicrosoft: () => Promise; + /** Adopt a session produced elsewhere (invite acceptance, demo switch). */ + adoptSession: (session: Session) => void; + /** Re-fetch the session after profile changes. */ + refresh: () => Promise; + signOut: () => Promise; +} + +const SessionContext = createContext(null); + +export function SessionProvider({ children }: { children: ReactNode }) { + const [session, setSession] = useState(null); + const [initializing, setInitializing] = useState(true); + const queryClient = useQueryClient(); + + useEffect(() => { + api + .getSession() + .then(setSession) + .finally(() => setInitializing(false)); + }, []); + + const adoptSession = useCallback( + (next: Session) => { + // a different identity invalidates everything previously fetched + queryClient.clear(); + setSession(next); + }, + [queryClient], + ); + + const signIn = useCallback( + async (email: string, password: string) => { + const next = await api.login(email, password); + adoptSession(next); + return next; + }, + [adoptSession], + ); + + const signInWithMicrosoft = useCallback(async () => { + const next = await api.loginWithMicrosoft(); + adoptSession(next); + return next; + }, [adoptSession]); + + const refresh = useCallback(async () => { + setSession(await api.getSession()); + }, []); + + const signOut = useCallback(async () => { + await api.logout(); + queryClient.clear(); + setSession(null); + }, [queryClient]); + + const value = useMemo( + () => ({ + session, + initializing, + can: (permission) => session?.permissions.includes(permission) ?? false, + signIn, + signInWithMicrosoft, + adoptSession, + refresh, + signOut, + }), + [session, initializing, signIn, signInWithMicrosoft, adoptSession, refresh, signOut], + ); + + return {children}; +} + +export function useSession(): SessionContextValue { + const ctx = useContext(SessionContext); + if (!ctx) throw new Error("useSession must be used inside "); + return ctx; +} diff --git a/SW.Bitween.Web/ClientApp/src/auth/guards.tsx b/SW.Bitween.Web/ClientApp/src/auth/guards.tsx new file mode 100644 index 00000000..d87db0e2 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/auth/guards.tsx @@ -0,0 +1,68 @@ +import type { ReactNode } from "react"; +import { Navigate, Outlet, useLocation } from "react-router"; +import { Lock } from "lucide-react"; +import type { PermissionKey } from "../api"; +import { labelIn, usePermissionCatalog } from "../api/permissions"; +import { useSession } from "./SessionContext"; + +/** Redirects to /login when signed out; shows a splash while checking. */ +export function RequireAuth() { + const { session, initializing } = useSession(); + const location = useLocation(); + + if (initializing) { + return ( +
+ Bitween +
+ ); + } + if (!session) { + return ; + } + return ; +} + +/** Full-page stop for routes the session's roles don't unlock. */ +export function AccessDenied({ permission }: { permission: PermissionKey }) { + // Falls back to the raw key until the catalog lands — never blocks the message. + const label = labelIn(usePermissionCatalog().data ?? [], permission); + return ( +
+ + + +

You don't have access to this page

+

+ Seeing it needs the {label}{" "} + permission, which none of your roles grant. An administrator can change that under Team → Roles. +

+
+ ); +} + +/** Route-level permission gate. */ +export function RequirePermission({ + permission, + children, +}: { + permission: PermissionKey; + children: ReactNode; +}) { + const { can } = useSession(); + if (!can(permission)) return ; + return <>{children}; +} + +/** Inline gate: unauthorized actions are hidden, never dimmed. */ +export function Can({ permission, children }: { permission: PermissionKey; children: ReactNode }) { + const { can } = useSession(); + if (!can(permission)) return null; + return <>{children}; +} + +/** Convenience hook for components that branch on a permission. */ +export function useSessionCan(permission: PermissionKey): boolean { + const { can } = useSession(); + return can(permission); +} diff --git a/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx b/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx new file mode 100644 index 00000000..0156c04a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx @@ -0,0 +1,544 @@ +import { useEffect, useRef, useState } from "react"; +import { Link } from "react-router"; +import { useQuery } from "@tanstack/react-query"; +import { ArrowUpRight, Braces, ChevronDown, ChevronRight, Search } from "lucide-react"; +import { api, type AdapterInfo, type AdapterKind, type PartnerRow } from "../../api"; +import { Button } from "../ui/basics"; +import { Field } from "../ui/forms"; +import { SearchSelect } from "../ui/SearchSelect"; + +/** What the picker is choosing, named for the band above the fields. */ +const KIND_LABELS: Record = { + receiver: "Receiver", + validator: "Validator", + mapper: "Mapper", + handler: "Handler", +}; + +export function useAdapterCatalog(kind: AdapterKind) { + return useQuery({ + queryKey: ["adapters", kind], + queryFn: () => api.listAdapters(kind), + staleTime: Infinity, + }); +} + +interface ReferenceToken { + label: string; + token: string; + /** Globals resolve to one literal value; partner properties resolve per-exchange. */ + value?: string; +} + +/** All insertable reference tokens: every global key + every known partner property. */ +function useReferenceTokens() { + const sets = useQuery({ queryKey: ["value-sets"], queryFn: () => api.listValueSets(), staleTime: 60_000 }); + const partners = useQuery({ queryKey: ["partners"], queryFn: () => api.listPartners(), staleTime: 60_000 }); + const globals: ReferenceToken[] = (sets.data ?? []).flatMap((s) => + Object.entries(s.values).map(([key, value]) => ({ + label: `${s.id}.${key}`, + token: `{{globals.${s.id}.${key}}}`, + value, + })), + ); + // propertyKeys, not adapterProperties: the list endpoint deliberately withholds + // property values (they can be secrets) and sends only their names, which is all + // a reference token needs. + const partnerKeys: ReferenceToken[] = [ + ...new Set((partners.data ?? []).flatMap((p) => p.propertyKeys)), + ].map((key) => ({ label: `partner.${key}`, token: `{{partner.${key}}}` })); + return { globals, partnerKeys, partners: partners.data ?? [] }; +} + +/** + * One partner's value for a `{{partner.KEY}}` reference. + * + * Fetched per partner, and only once the row is expanded. The partners list + * deliberately carries property names without their values, so that something like + * an FTP password isn't sitting in the browser on every adapter screen; this pulls + * the value on demand, for the handful of partners actually being inspected. + */ +function PartnerPropValue({ partnerId, propKey }: { partnerId: number; propKey: string }) { + const props = useQuery({ + queryKey: ["partner-adapter-properties", partnerId], + queryFn: () => api.getPartnerAdapterProperties(partnerId), + staleTime: 60_000, + }); + if (props.isPending) return loading…; + const value = props.data?.[propKey]; + return ( + + {value === undefined || value === "" ? "—" : value} + + ); +} + +/** + * Searchable popover for inserting a `{{globals.…}}` / `{{partner.…}}` + * reference. Globals show their literal value; partner properties resolve + * per-exchange, so they show a note instead. + */ +function ReferenceMenu({ + globals, + partnerKeys, + onPick, + label, +}: { + globals: ReferenceToken[]; + partnerKeys: ReferenceToken[]; + onPick: (token: string) => void; + label: string; +}) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + const onDown = (e: MouseEvent) => { + if (!ref.current?.contains(e.target as Node)) setOpen(false); + }; + const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false); + document.addEventListener("mousedown", onDown); + document.addEventListener("keydown", onKey); + return () => { + document.removeEventListener("mousedown", onDown); + document.removeEventListener("keydown", onKey); + }; + }, [open]); + + const needle = query.trim().toLowerCase(); + const matches = (t: ReferenceToken) => + !needle || t.label.toLowerCase().includes(needle) || (t.value?.toLowerCase().includes(needle) ?? false); + const filteredGlobals = globals.filter(matches); + const filteredPartnerKeys = partnerKeys.filter(matches); + const noMatches = filteredGlobals.length === 0 && filteredPartnerKeys.length === 0; + + const pick = (token: string) => { + onPick(token); + setOpen(false); + setQuery(""); + }; + + return ( +
+ + {open && ( +
+
+ + setQuery(e.target.value)} + placeholder="Search references" + aria-label="Search references" + className="h-8 w-full rounded-md border border-ink-200 bg-white pr-2 pl-8 text-[13px] placeholder:text-ink-400 focus:border-crimson-400 focus:ring-2 focus:ring-crimson-100 focus:outline-none" + /> +
+
+ {noMatches &&

No matches.

} + {filteredGlobals.length > 0 && ( +
+

+ Global values +

+ {filteredGlobals.map((t) => ( + + ))} +
+ )} + {filteredPartnerKeys.length > 0 && ( +
+

+ Partner properties +

+ {filteredPartnerKeys.map((t) => ( + + ))} +
+ )} +
+
+ )} +
+ ); +} + +/** + * What the references inside a field's value resolve to, shown right under + * it: globals show their literal value (linking to the set); partner + * properties show how many partners define them and drill down to every + * partner's value. + */ +function ReferenceHints({ + value, + globals, + partners, +}: { + value: string; + globals: ReferenceToken[]; + partners: PartnerRow[]; +}) { + const [openKeys, setOpenKeys] = useState>(new Set()); + + + // Set ids and keys are free-form (spaces included) on the backend — match on the + // `{{…}}` delimiters themselves, not a restrictive charset, or refs with a space + // in the id/key (e.g. "schedule source url") silently fail to match here. + const globalRefs = [...new Set([...value.matchAll(/\{\{globals\.[^}]+\}\}/g)].map((m) => m[0]))]; + const partnerRefs = [...new Set([...value.matchAll(/\{\{partner\.([^}]+)\}\}/g)].map((m) => m[1]))]; + if (globalRefs.length === 0 && partnerRefs.length === 0) return null; + + const realPartners = partners.filter((p) => !p.isSystem); + const toggle = (key: string) => + setOpenKeys((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + + return ( +
+ {globalRefs.map((token) => { + const ref = globals.find((g) => g.token === token); + const setId = token.match(/\{\{globals\.([^.]+)\./)?.[1]; + return ( +
+ + {token} + + + {ref ? ( + + {ref.value} + + ) : ( + not found — check the set and key + )} +
+ ); + })} + + {partnerRefs.map((key) => { + const withValue = realPartners.filter((p) => p.propertyKeys.includes(key)); + const without = realPartners.length - withValue.length; + const open = openKeys.has(key); + return ( +
+ + {open && ( +
    + {withValue.map((p) => ( +
  • + + {p.name} + + + +
  • + ))} + {without > 0 && ( +
  • + {without} partner{without === 1 ? " doesn't" : "s don't"} set it — their exchanges resolve it + empty. +
  • + )} +
+ )} +
+ ); + })} +
+ ); +} + +/** One adapter property: grows with content, can insert reference tokens. */ +function PropField({ + prop, + value, + disabled, + onChange, +}: { + prop: AdapterInfo["props"][number]; + value: string; + disabled: boolean; + onChange: (value: string) => void; +}) { + const { globals, partnerKeys, partners } = useReferenceTokens(); + const textareaRef = useRef(null); + + // Whether the user is part-way through entering a value. Masking has to key off + // this rather than off whether the field currently holds text: the latter flipped + // to masked on the very first keystroke, so typing a secret hid what you'd typed. + const [entering, setEntering] = useState(false); + const lastEmitted = useRef(null); + + // The parent re-supplies values from the server after a save or a discard. When + // what arrives isn't what this field last emitted, the draft was reset from the + // outside, so a stored secret goes back to being masked. + useEffect(() => { + if (value !== lastEmitted.current) setEntering(false); + }, [value]); + + const emit = (next: string) => { + lastEmitted.current = next; + setEntering(true); + onChange(next); + }; + + const masked = prop.secret && !!value && !entering; + + // Insert at the cursor (or over the current selection) instead of always + // appending, so picking a second reference doesn't just tack it onto the end. + const insertToken = (token: string) => { + const el = textareaRef.current; + const start = el?.selectionStart ?? value.length; + const end = el?.selectionEnd ?? value.length; + emit(value.slice(0, start) + token + value.slice(end)); + const caret = start + token.length; + requestAnimationFrame(() => { + el?.focus(); + el?.setSelectionRange(caret, caret); + }); + }; + + return ( + + {masked ? ( +
+ •••••••• + {!disabled && ( + + )} +
+ ) : ( +
+
+