feat: Implement SendEngine database context and migrations
- Added SendEngineDbContext for managing database interactions. - Created SendEngineDbContextFactory for design-time database context creation. - Established dependency injection for the infrastructure layer. - Defined entity configurations for Tenant, MailingList, Subscriber, ListMember, EventInbox, Campaign, SendJob, SendBatch, DeliverySummary, AuthClient, AuthClientKey, and WebhookNonce. - Generated initial database migration snapshot. - Implemented installer program for database migration commands.
This commit is contained in:
parent
5f749af298
commit
e9712fb1f7
@ -2,3 +2,10 @@ ASPNETCORE_ENVIRONMENT=Development
|
|||||||
ConnectionStrings__Default=Host=localhost;Database=send_engine;Username=postgres;Password=postgres
|
ConnectionStrings__Default=Host=localhost;Database=send_engine;Username=postgres;Password=postgres
|
||||||
ESP__Provider=ses
|
ESP__Provider=ses
|
||||||
ESP__ApiKey=change_me
|
ESP__ApiKey=change_me
|
||||||
|
Db__AutoMigrate=true
|
||||||
|
Jwt__Issuer=member_center
|
||||||
|
Jwt__Audience=send_engine
|
||||||
|
Jwt__SigningKey=change_me_jwt_signing_key
|
||||||
|
Webhook__Secrets__member_center=change_me_webhook_secret
|
||||||
|
Webhook__TimestampSkewSeconds=300
|
||||||
|
Ses__SkipSignatureValidation=true
|
||||||
|
|||||||
@ -50,6 +50,9 @@ mass_mail_engine/
|
|||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
使用 VS Code `Run Build Task`(預設執行 `dotnet build SendEngine.sln`)。
|
||||||
|
|
||||||
## 待確認事項
|
## 待確認事項
|
||||||
- 事件系統選擇(Kafka/RabbitMQ/SNS+SQS / Webhook)
|
- 事件系統選擇(Kafka/RabbitMQ/SNS+SQS / Webhook)
|
||||||
- ESP 優先順序(SES / SendGrid / Mailgun)
|
- ESP 優先順序(SES / SendGrid / Mailgun)
|
||||||
|
|||||||
@ -1,4 +1,55 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
VisualStudioVersion = 17.0.31903.59
|
# Visual Studio Version 17
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AD7B89A4-26BD-4FFB-B8AB-A3A03CE4AD03}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SendEngine.Api", "src\SendEngine.Api\SendEngine.Api.csproj", "{BBD2426B-0DA0-4711-80AA-04E93AAEEDA6}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SendEngine.Domain", "src\SendEngine.Domain\SendEngine.Domain.csproj", "{B8EA34E2-E68E-436B-B559-BFB61C019A48}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SendEngine.Application", "src\SendEngine.Application\SendEngine.Application.csproj", "{192589C9-6D2D-4BEE-9D36-E45A8B50D844}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SendEngine.Infrastructure", "src\SendEngine.Infrastructure\SendEngine.Infrastructure.csproj", "{B0D8BA24-3F27-4CDB-8FC0-058F62BBCDF5}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SendEngine.Installer", "src\SendEngine.Installer\SendEngine.Installer.csproj", "{449F726D-6D8D-4B8D-9474-A4AD9C840000}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{BBD2426B-0DA0-4711-80AA-04E93AAEEDA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BBD2426B-0DA0-4711-80AA-04E93AAEEDA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BBD2426B-0DA0-4711-80AA-04E93AAEEDA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BBD2426B-0DA0-4711-80AA-04E93AAEEDA6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B8EA34E2-E68E-436B-B559-BFB61C019A48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B8EA34E2-E68E-436B-B559-BFB61C019A48}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B8EA34E2-E68E-436B-B559-BFB61C019A48}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B8EA34E2-E68E-436B-B559-BFB61C019A48}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{192589C9-6D2D-4BEE-9D36-E45A8B50D844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{192589C9-6D2D-4BEE-9D36-E45A8B50D844}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{192589C9-6D2D-4BEE-9D36-E45A8B50D844}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{192589C9-6D2D-4BEE-9D36-E45A8B50D844}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B0D8BA24-3F27-4CDB-8FC0-058F62BBCDF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B0D8BA24-3F27-4CDB-8FC0-058F62BBCDF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B0D8BA24-3F27-4CDB-8FC0-058F62BBCDF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B0D8BA24-3F27-4CDB-8FC0-058F62BBCDF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{449F726D-6D8D-4B8D-9474-A4AD9C840000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{449F726D-6D8D-4B8D-9474-A4AD9C840000}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{449F726D-6D8D-4B8D-9474-A4AD9C840000}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{449F726D-6D8D-4B8D-9474-A4AD9C840000}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{BBD2426B-0DA0-4711-80AA-04E93AAEEDA6} = {AD7B89A4-26BD-4FFB-B8AB-A3A03CE4AD03}
|
||||||
|
{B8EA34E2-E68E-436B-B559-BFB61C019A48} = {AD7B89A4-26BD-4FFB-B8AB-A3A03CE4AD03}
|
||||||
|
{192589C9-6D2D-4BEE-9D36-E45A8B50D844} = {AD7B89A4-26BD-4FFB-B8AB-A3A03CE4AD03}
|
||||||
|
{B0D8BA24-3F27-4CDB-8FC0-058F62BBCDF5} = {AD7B89A4-26BD-4FFB-B8AB-A3A03CE4AD03}
|
||||||
|
{449F726D-6D8D-4B8D-9474-A4AD9C840000} = {AD7B89A4-26BD-4FFB-B8AB-A3A03CE4AD03}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
|
|||||||
@ -2,3 +2,7 @@
|
|||||||
|
|
||||||
- 需求:.NET SDK 8.x, PostgreSQL
|
- 需求:.NET SDK 8.x, PostgreSQL
|
||||||
- 設定:複製 `.env.example` → `.env`
|
- 設定:複製 `.env.example` → `.env`
|
||||||
|
- Migration:
|
||||||
|
- 預設由 API 啟動時自動執行(`Db__AutoMigrate=true`)
|
||||||
|
- 需要關閉時請設定 `Db__AutoMigrate=false`
|
||||||
|
- 手動執行可用 `dotnet run --project src/SendEngine.Installer -- migrate`
|
||||||
|
|||||||
@ -22,11 +22,13 @@ Header 建議:
|
|||||||
- `X-Signature`: `hex(hmac_sha256(secret, body))`
|
- `X-Signature`: `hex(hmac_sha256(secret, body))`
|
||||||
- `X-Timestamp`: Unix epoch seconds
|
- `X-Timestamp`: Unix epoch seconds
|
||||||
- `X-Nonce`: UUID
|
- `X-Nonce`: UUID
|
||||||
|
- `X-Client-Id`: Auth client UUID(對應 `auth_clients.id`)
|
||||||
|
|
||||||
驗證規則:
|
驗證規則:
|
||||||
- timestamp 在允許時間窗內(例如 ±5 分鐘)
|
- timestamp 在允許時間窗內(例如 ±5 分鐘)
|
||||||
- nonce 不可重複(重放防護)
|
- nonce 不可重複(重放防護)
|
||||||
- signature 必須匹配
|
- signature 必須匹配
|
||||||
|
- client 必須存在且為 active
|
||||||
|
|
||||||
### 3. Send Engine → Member Center 回寫
|
### 3. Send Engine → Member Center 回寫
|
||||||
使用 OAuth2 Client Credentials(Send Engine 作為 client)
|
使用 OAuth2 Client Credentials(Send Engine 作為 client)
|
||||||
@ -205,7 +207,7 @@ Endpoint:
|
|||||||
- `POST /webhooks/ses`
|
- `POST /webhooks/ses`
|
||||||
|
|
||||||
驗證:
|
驗證:
|
||||||
- 依 SES/SNS 規格驗簽
|
- 依 SES/SNS 規格驗簽(可用 `Ses__SkipSignatureValidation=true` 暫時略過)
|
||||||
|
|
||||||
Request Body(示意):
|
Request Body(示意):
|
||||||
```json
|
```json
|
||||||
|
|||||||
@ -135,6 +135,7 @@ paths:
|
|||||||
- webhookSignature: []
|
- webhookSignature: []
|
||||||
webhookTimestamp: []
|
webhookTimestamp: []
|
||||||
webhookNonce: []
|
webhookNonce: []
|
||||||
|
webhookClientId: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -176,6 +177,7 @@ paths:
|
|||||||
- webhookSignature: []
|
- webhookSignature: []
|
||||||
webhookTimestamp: []
|
webhookTimestamp: []
|
||||||
webhookNonce: []
|
webhookNonce: []
|
||||||
|
webhookClientId: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -255,6 +257,10 @@ components:
|
|||||||
type: apiKey
|
type: apiKey
|
||||||
in: header
|
in: header
|
||||||
name: X-Nonce
|
name: X-Nonce
|
||||||
|
webhookClientId:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-Client-Id
|
||||||
sesSignature:
|
sesSignature:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
in: header
|
in: header
|
||||||
|
|||||||
48
src/SendEngine.Api/Models/SendJobModels.cs
Normal file
48
src/SendEngine.Api/Models/SendJobModels.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SendEngine.Api.Models;
|
||||||
|
|
||||||
|
public sealed class CreateSendJobRequest
|
||||||
|
{
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid ListId { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Subject { get; set; }
|
||||||
|
public string? BodyHtml { get; set; }
|
||||||
|
public string? BodyText { get; set; }
|
||||||
|
public JsonElement? Template { get; set; }
|
||||||
|
public DateTimeOffset? ScheduledAt { get; set; }
|
||||||
|
public DateTimeOffset? WindowStart { get; set; }
|
||||||
|
public DateTimeOffset? WindowEnd { get; set; }
|
||||||
|
public TrackingOptions? Tracking { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TrackingOptions
|
||||||
|
{
|
||||||
|
public bool? Open { get; set; }
|
||||||
|
public bool? Click { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CreateSendJobResponse
|
||||||
|
{
|
||||||
|
public Guid SendJobId { get; set; }
|
||||||
|
public string Status { get; set; } = "pending";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SendJobResponse
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid ListId { get; set; }
|
||||||
|
public Guid CampaignId { get; set; }
|
||||||
|
public string Status { get; set; } = "pending";
|
||||||
|
public DateTimeOffset? ScheduledAt { get; set; }
|
||||||
|
public DateTimeOffset? WindowStart { get; set; }
|
||||||
|
public DateTimeOffset? WindowEnd { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SendJobStatusResponse
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
40
src/SendEngine.Api/Models/WebhookModels.cs
Normal file
40
src/SendEngine.Api/Models/WebhookModels.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
namespace SendEngine.Api.Models;
|
||||||
|
|
||||||
|
public sealed class SubscriptionEventRequest
|
||||||
|
{
|
||||||
|
public Guid EventId { get; set; }
|
||||||
|
public string EventType { get; set; } = string.Empty;
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid ListId { get; set; }
|
||||||
|
public SubscriberPayload Subscriber { get; set; } = new();
|
||||||
|
public DateTimeOffset OccurredAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SubscriberPayload
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public Dictionary<string, object>? Preferences { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FullSyncBatchRequest
|
||||||
|
{
|
||||||
|
public Guid SyncId { get; set; }
|
||||||
|
public int BatchNo { get; set; }
|
||||||
|
public int BatchTotal { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid ListId { get; set; }
|
||||||
|
public List<SubscriberPayload> Subscribers { get; set; } = new();
|
||||||
|
public DateTimeOffset OccurredAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SesEventRequest
|
||||||
|
{
|
||||||
|
public string EventType { get; set; } = string.Empty;
|
||||||
|
public string MessageId { get; set; } = string.Empty;
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string? BounceType { get; set; }
|
||||||
|
public DateTimeOffset OccurredAt { get; set; }
|
||||||
|
}
|
||||||
286
src/SendEngine.Api/Program.cs
Normal file
286
src/SendEngine.Api/Program.cs
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using SendEngine.Api.Models;
|
||||||
|
using SendEngine.Api.Security;
|
||||||
|
using SendEngine.Domain.Entities;
|
||||||
|
using SendEngine.Infrastructure;
|
||||||
|
using SendEngine.Infrastructure.Data;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
builder.Services.AddInfrastructure(builder.Configuration);
|
||||||
|
|
||||||
|
var signingKey = builder.Configuration["Jwt:SigningKey"];
|
||||||
|
if (string.IsNullOrWhiteSpace(signingKey))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Jwt:SigningKey is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
||||||
|
ValidAudience = builder.Configuration["Jwt:Audience"],
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
var autoMigrate = builder.Configuration.GetValue("Db:AutoMigrate", true);
|
||||||
|
if (autoMigrate)
|
||||||
|
{
|
||||||
|
using var scope = app.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<SendEngineDbContext>();
|
||||||
|
db.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapGet("/health", () => Results.Ok(new { status = "ok" }))
|
||||||
|
.WithName("Health")
|
||||||
|
.WithOpenApi();
|
||||||
|
|
||||||
|
app.MapPost("/api/send-jobs", async (HttpContext httpContext, CreateSendJobRequest request, SendEngineDbContext db) =>
|
||||||
|
{
|
||||||
|
var tenantId = GetTenantId(httpContext.User);
|
||||||
|
if (tenantId is null)
|
||||||
|
{
|
||||||
|
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.TenantId == Guid.Empty)
|
||||||
|
{
|
||||||
|
request.TenantId = tenantId.Value;
|
||||||
|
}
|
||||||
|
else if (request.TenantId != tenantId.Value)
|
||||||
|
{
|
||||||
|
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ListId == Guid.Empty)
|
||||||
|
{
|
||||||
|
return Results.UnprocessableEntity(new { error = "list_id_required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Subject))
|
||||||
|
{
|
||||||
|
return Results.UnprocessableEntity(new { error = "subject_required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasContent = !string.IsNullOrWhiteSpace(request.BodyHtml)
|
||||||
|
|| !string.IsNullOrWhiteSpace(request.BodyText)
|
||||||
|
|| request.Template.HasValue;
|
||||||
|
|
||||||
|
if (!hasContent)
|
||||||
|
{
|
||||||
|
return Results.UnprocessableEntity(new { error = "content_required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.WindowStart.HasValue && request.WindowEnd.HasValue
|
||||||
|
&& request.WindowStart.Value >= request.WindowEnd.Value)
|
||||||
|
{
|
||||||
|
return Results.UnprocessableEntity(new { error = "window_invalid" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var campaign = new Campaign
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
ListId = request.ListId,
|
||||||
|
Name = request.Name,
|
||||||
|
Subject = request.Subject,
|
||||||
|
BodyHtml = request.BodyHtml,
|
||||||
|
BodyText = request.BodyText,
|
||||||
|
Template = request.Template.HasValue ? request.Template.Value.GetRawText() : null,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var sendJob = new SendJob
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
ListId = request.ListId,
|
||||||
|
CampaignId = campaign.Id,
|
||||||
|
ScheduledAt = request.ScheduledAt,
|
||||||
|
WindowStart = request.WindowStart,
|
||||||
|
WindowEnd = request.WindowEnd,
|
||||||
|
Status = "pending",
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
db.Campaigns.Add(campaign);
|
||||||
|
db.SendJobs.Add(sendJob);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Ok(new CreateSendJobResponse
|
||||||
|
{
|
||||||
|
SendJobId = sendJob.Id,
|
||||||
|
Status = sendJob.Status
|
||||||
|
});
|
||||||
|
}).RequireAuthorization().WithName("CreateSendJob").WithOpenApi();
|
||||||
|
|
||||||
|
app.MapGet("/api/send-jobs/{id:guid}", async (HttpContext httpContext, Guid id, SendEngineDbContext db) =>
|
||||||
|
{
|
||||||
|
var tenantId = GetTenantId(httpContext.User);
|
||||||
|
if (tenantId is null)
|
||||||
|
{
|
||||||
|
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendJob = await db.SendJobs.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId.Value);
|
||||||
|
if (sendJob is null)
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok(new SendJobResponse
|
||||||
|
{
|
||||||
|
Id = sendJob.Id,
|
||||||
|
TenantId = sendJob.TenantId,
|
||||||
|
ListId = sendJob.ListId,
|
||||||
|
CampaignId = sendJob.CampaignId,
|
||||||
|
Status = sendJob.Status,
|
||||||
|
ScheduledAt = sendJob.ScheduledAt,
|
||||||
|
WindowStart = sendJob.WindowStart,
|
||||||
|
WindowEnd = sendJob.WindowEnd
|
||||||
|
});
|
||||||
|
}).RequireAuthorization().WithName("GetSendJob").WithOpenApi();
|
||||||
|
|
||||||
|
app.MapPost("/api/send-jobs/{id:guid}/cancel", async (HttpContext httpContext, Guid id, SendEngineDbContext db) =>
|
||||||
|
{
|
||||||
|
var tenantId = GetTenantId(httpContext.User);
|
||||||
|
if (tenantId is null)
|
||||||
|
{
|
||||||
|
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendJob = await db.SendJobs.FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId.Value);
|
||||||
|
if (sendJob is null)
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendJob.Status = "cancelled";
|
||||||
|
sendJob.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Ok(new SendJobStatusResponse { Id = sendJob.Id, Status = sendJob.Status });
|
||||||
|
}).RequireAuthorization().WithName("CancelSendJob").WithOpenApi();
|
||||||
|
|
||||||
|
app.MapPost("/webhooks/subscriptions", async (HttpContext httpContext, SubscriptionEventRequest request, SendEngineDbContext db) =>
|
||||||
|
{
|
||||||
|
var secret = builder.Configuration["Webhook:Secrets:member_center"] ?? string.Empty;
|
||||||
|
var skewSeconds = builder.Configuration.GetValue("Webhook:TimestampSkewSeconds", 300);
|
||||||
|
var validation = await WebhookValidator.ValidateAsync(httpContext, db, secret, skewSeconds);
|
||||||
|
if (validation is not null)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(request);
|
||||||
|
|
||||||
|
var inbox = new EventInbox
|
||||||
|
{
|
||||||
|
Id = request.EventId == Guid.Empty ? Guid.NewGuid() : request.EventId,
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
EventType = request.EventType,
|
||||||
|
Source = "member_center",
|
||||||
|
Payload = payload,
|
||||||
|
ReceivedAt = DateTimeOffset.UtcNow,
|
||||||
|
Status = "received"
|
||||||
|
};
|
||||||
|
|
||||||
|
db.EventsInbox.Add(inbox);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}).WithName("SubscriptionWebhook").WithOpenApi();
|
||||||
|
|
||||||
|
app.MapPost("/webhooks/lists/full-sync", async (HttpContext httpContext, FullSyncBatchRequest request, SendEngineDbContext db) =>
|
||||||
|
{
|
||||||
|
var secret = builder.Configuration["Webhook:Secrets:member_center"] ?? string.Empty;
|
||||||
|
var skewSeconds = builder.Configuration.GetValue("Webhook:TimestampSkewSeconds", 300);
|
||||||
|
var validation = await WebhookValidator.ValidateAsync(httpContext, db, secret, skewSeconds);
|
||||||
|
if (validation is not null)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(request);
|
||||||
|
|
||||||
|
var inbox = new EventInbox
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
EventType = "list.full_sync",
|
||||||
|
Source = "member_center",
|
||||||
|
Payload = payload,
|
||||||
|
ReceivedAt = DateTimeOffset.UtcNow,
|
||||||
|
Status = "received"
|
||||||
|
};
|
||||||
|
|
||||||
|
db.EventsInbox.Add(inbox);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}).WithName("FullSyncWebhook").WithOpenApi();
|
||||||
|
|
||||||
|
app.MapPost("/webhooks/ses", async (HttpContext httpContext, SesEventRequest request, SendEngineDbContext db) =>
|
||||||
|
{
|
||||||
|
var skipValidation = builder.Configuration.GetValue("Ses:SkipSignatureValidation", true);
|
||||||
|
var sesSignature = httpContext.Request.Headers["X-Amz-Sns-Signature"].ToString();
|
||||||
|
if (!skipValidation && string.IsNullOrWhiteSpace(sesSignature))
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(request);
|
||||||
|
|
||||||
|
var inbox = new EventInbox
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
EventType = $"ses.{request.EventType}",
|
||||||
|
Source = "ses",
|
||||||
|
Payload = payload,
|
||||||
|
ReceivedAt = DateTimeOffset.UtcNow,
|
||||||
|
Status = "received"
|
||||||
|
};
|
||||||
|
|
||||||
|
db.EventsInbox.Add(inbox);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}).WithName("SesWebhook").WithOpenApi();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
static Guid? GetTenantId(ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var value = user.FindFirst("tenant_id")?.Value;
|
||||||
|
return Guid.TryParse(value, out var tenantId) ? tenantId : null;
|
||||||
|
}
|
||||||
41
src/SendEngine.Api/Properties/launchSettings.json
Normal file
41
src/SendEngine.Api/Properties/launchSettings.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:45678",
|
||||||
|
"sslPort": 44357
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5024",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7225;http://localhost:5024",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/SendEngine.Api/Security/WebhookValidator.cs
Normal file
95
src/SendEngine.Api/Security/WebhookValidator.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SendEngine.Infrastructure.Data;
|
||||||
|
using SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SendEngine.Api.Security;
|
||||||
|
|
||||||
|
public static class WebhookValidator
|
||||||
|
{
|
||||||
|
public static async Task<IResult?> ValidateAsync(HttpContext context, SendEngineDbContext db, string secret, int maxSkewSeconds)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(secret))
|
||||||
|
{
|
||||||
|
return Results.StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
var signature = context.Request.Headers["X-Signature"].ToString();
|
||||||
|
var timestampHeader = context.Request.Headers["X-Timestamp"].ToString();
|
||||||
|
var nonce = context.Request.Headers["X-Nonce"].ToString();
|
||||||
|
var clientIdHeader = context.Request.Headers["X-Client-Id"].ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(signature) || string.IsNullOrWhiteSpace(timestampHeader)
|
||||||
|
|| string.IsNullOrWhiteSpace(nonce) || string.IsNullOrWhiteSpace(clientIdHeader))
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!long.TryParse(timestampHeader, out var timestampSeconds))
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var nowSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
if (Math.Abs(nowSeconds - timestampSeconds) > maxSkewSeconds)
|
||||||
|
{
|
||||||
|
return Results.StatusCode(StatusCodes.Status401Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Guid.TryParse(clientIdHeader, out var clientId))
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasClient = await db.AuthClients.AsNoTracking().AnyAsync(x => x.Id == clientId);
|
||||||
|
if (!hasClient)
|
||||||
|
{
|
||||||
|
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Request.EnableBuffering();
|
||||||
|
using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true);
|
||||||
|
var body = await reader.ReadToEndAsync();
|
||||||
|
context.Request.Body.Position = 0;
|
||||||
|
|
||||||
|
var expected = ComputeHmacHex(secret, body);
|
||||||
|
if (!FixedTimeEquals(expected, signature))
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasNonce = await db.WebhookNonces.AsNoTracking().AnyAsync(x => x.ClientId == clientId && x.Nonce == nonce);
|
||||||
|
if (hasNonce)
|
||||||
|
{
|
||||||
|
return Results.Conflict(new { error = "replay_detected" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonceEntry = new WebhookNonce
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ClientId = clientId,
|
||||||
|
Nonce = nonce,
|
||||||
|
ReceivedAt = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
db.WebhookNonces.Add(nonceEntry);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeHmacHex(string secret, string payload)
|
||||||
|
{
|
||||||
|
var key = Encoding.UTF8.GetBytes(secret);
|
||||||
|
using var hmac = new HMACSHA256(key);
|
||||||
|
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool FixedTimeEquals(string a, string b)
|
||||||
|
{
|
||||||
|
var aBytes = Encoding.UTF8.GetBytes(a);
|
||||||
|
var bBytes = Encoding.UTF8.GetBytes(b);
|
||||||
|
return aBytes.Length == bBytes.Length && CryptographicOperations.FixedTimeEquals(aBytes, bBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/SendEngine.Api/SendEngine.Api.csproj
Normal file
20
src/SendEngine.Api/SendEngine.Api.csproj
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.23" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SendEngine.Application\SendEngine.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\SendEngine.Infrastructure\SendEngine.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
src/SendEngine.Api/SendEngine.Api.http
Normal file
6
src/SendEngine.Api/SendEngine.Api.http
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@SendEngine.Api_HostAddress = http://localhost:5024
|
||||||
|
|
||||||
|
GET {{SendEngine.Api_HostAddress}}/health
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
9
src/SendEngine.Api/appsettings.json
Normal file
9
src/SendEngine.Api/appsettings.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
13
src/SendEngine.Application/SendEngine.Application.csproj
Normal file
13
src/SendEngine.Application/SendEngine.Application.csproj
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SendEngine.Domain\SendEngine.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
12
src/SendEngine.Domain/Entities/AuthClient.cs
Normal file
12
src/SendEngine.Domain/Entities/AuthClient.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class AuthClient
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
|
public string ClientId { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string[] Scopes { get; set; } = Array.Empty<string>();
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
10
src/SendEngine.Domain/Entities/AuthClientKey.cs
Normal file
10
src/SendEngine.Domain/Entities/AuthClientKey.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class AuthClientKey
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid ClientId { get; set; }
|
||||||
|
public string KeyHash { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset? RevokedAt { get; set; }
|
||||||
|
}
|
||||||
14
src/SendEngine.Domain/Entities/Campaign.cs
Normal file
14
src/SendEngine.Domain/Entities/Campaign.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class Campaign
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid ListId { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Subject { get; set; }
|
||||||
|
public string? BodyHtml { get; set; }
|
||||||
|
public string? BodyText { get; set; }
|
||||||
|
public string? Template { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
14
src/SendEngine.Domain/Entities/DeliverySummary.cs
Normal file
14
src/SendEngine.Domain/Entities/DeliverySummary.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class DeliverySummary
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid SendJobId { get; set; }
|
||||||
|
public int Total { get; set; }
|
||||||
|
public int Delivered { get; set; }
|
||||||
|
public int Bounced { get; set; }
|
||||||
|
public int Complained { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
14
src/SendEngine.Domain/Entities/EventInbox.cs
Normal file
14
src/SendEngine.Domain/Entities/EventInbox.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class EventInbox
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public string EventType { get; set; } = string.Empty;
|
||||||
|
public string Source { get; set; } = string.Empty;
|
||||||
|
public string Payload { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset ReceivedAt { get; set; }
|
||||||
|
public DateTimeOffset? ProcessedAt { get; set; }
|
||||||
|
public string Status { get; set; } = "received";
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
12
src/SendEngine.Domain/Entities/ListMember.cs
Normal file
12
src/SendEngine.Domain/Entities/ListMember.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class ListMember
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid ListId { get; set; }
|
||||||
|
public Guid SubscriberId { get; set; }
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
9
src/SendEngine.Domain/Entities/MailingList.cs
Normal file
9
src/SendEngine.Domain/Entities/MailingList.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class MailingList
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
12
src/SendEngine.Domain/Entities/SendBatch.cs
Normal file
12
src/SendEngine.Domain/Entities/SendBatch.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class SendBatch
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid SendJobId { get; set; }
|
||||||
|
public string Status { get; set; } = "queued";
|
||||||
|
public int Size { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
15
src/SendEngine.Domain/Entities/SendJob.cs
Normal file
15
src/SendEngine.Domain/Entities/SendJob.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class SendJob
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public Guid ListId { get; set; }
|
||||||
|
public Guid CampaignId { get; set; }
|
||||||
|
public DateTimeOffset? ScheduledAt { get; set; }
|
||||||
|
public DateTimeOffset? WindowStart { get; set; }
|
||||||
|
public DateTimeOffset? WindowEnd { get; set; }
|
||||||
|
public string Status { get; set; } = "pending";
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
12
src/SendEngine.Domain/Entities/Subscriber.cs
Normal file
12
src/SendEngine.Domain/Entities/Subscriber.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class Subscriber
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public string? Preferences { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
8
src/SendEngine.Domain/Entities/Tenant.cs
Normal file
8
src/SendEngine.Domain/Entities/Tenant.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class Tenant
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
9
src/SendEngine.Domain/Entities/WebhookNonce.cs
Normal file
9
src/SendEngine.Domain/Entities/WebhookNonce.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class WebhookNonce
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid ClientId { get; set; }
|
||||||
|
public string Nonce { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset ReceivedAt { get; set; }
|
||||||
|
}
|
||||||
9
src/SendEngine.Domain/SendEngine.Domain.csproj
Normal file
9
src/SendEngine.Domain/SendEngine.Domain.csproj
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
686
src/SendEngine.Infrastructure/Data/Migrations/20260210083240_Initial.Designer.cs
generated
Normal file
686
src/SendEngine.Infrastructure/Data/Migrations/20260210083240_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,686 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using SendEngine.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SendEngine.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SendEngineDbContext))]
|
||||||
|
[Migration("20260210083240_Initial")]
|
||||||
|
partial class Initial
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.AuthClient", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string[]>("Scopes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("scopes");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_auth_clients_tenant");
|
||||||
|
|
||||||
|
b.ToTable("auth_clients", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.AuthClientKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("ClientId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("KeyHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("key_hash");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId")
|
||||||
|
.HasDatabaseName("idx_auth_client_keys_client");
|
||||||
|
|
||||||
|
b.ToTable("auth_client_keys", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Campaign", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("BodyHtml")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("body_html");
|
||||||
|
|
||||||
|
b.Property<string>("BodyText")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("body_text");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("ListId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string>("Subject")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("subject");
|
||||||
|
|
||||||
|
b.Property<string>("Template")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("template");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ListId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_campaigns_tenant");
|
||||||
|
|
||||||
|
b.ToTable("campaigns", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.DeliverySummary", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<int>("Bounced")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("bounced");
|
||||||
|
|
||||||
|
b.Property<int>("Complained")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("complained");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Delivered")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("delivered");
|
||||||
|
|
||||||
|
b.Property<Guid>("SendJobId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("send_job_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<int>("Total")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("total");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SendJobId")
|
||||||
|
.HasDatabaseName("idx_delivery_summary_job");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "SendJobId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("delivery_summary", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.EventInbox", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("Error")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("error");
|
||||||
|
|
||||||
|
b.Property<string>("EventType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("event_type");
|
||||||
|
|
||||||
|
b.Property<string>("Payload")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("payload");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ProcessedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("processed_at");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("received_at");
|
||||||
|
|
||||||
|
b.Property<string>("Source")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("source");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("EventType")
|
||||||
|
.HasDatabaseName("idx_events_inbox_type");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_events_inbox_status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_events_inbox_tenant");
|
||||||
|
|
||||||
|
b.ToTable("events_inbox", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.ListMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("ListId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("SubscriberId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("subscriber_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_list_members_list");
|
||||||
|
|
||||||
|
b.HasIndex("SubscriberId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_list_members_tenant");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "ListId", "SubscriberId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("list_members", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.MailingList", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_lists_tenant");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.SendBatch", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("SendJobId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("send_job_id");
|
||||||
|
|
||||||
|
b.Property<int>("Size")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("size");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SendJobId")
|
||||||
|
.HasDatabaseName("idx_send_batches_job");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_send_batches_status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("send_batches", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.SendJob", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("CampaignId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("campaign_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("ListId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ScheduledAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("scheduled_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("WindowEnd")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("window_end");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("WindowStart")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("window_start");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CampaignId");
|
||||||
|
|
||||||
|
b.HasIndex("ListId");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_send_jobs_status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_send_jobs_tenant");
|
||||||
|
|
||||||
|
b.ToTable("send_jobs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Subscriber", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("citext")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<string>("Preferences")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("preferences");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_subscribers_tenant");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "Email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("subscribers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Tenant", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("tenants", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.WebhookNonce", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("ClientId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<string>("Nonce")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("nonce");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("received_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId")
|
||||||
|
.HasDatabaseName("idx_webhook_nonces_client");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId", "Nonce")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("webhook_nonces", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.AuthClientKey", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.AuthClient", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ClientId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_auth_client_keys_client");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Campaign", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.MailingList", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_campaigns_list");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_campaigns_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.DeliverySummary", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.SendJob", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SendJobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_delivery_summary_job");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_delivery_summary_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.EventInbox", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_events_inbox_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.ListMember", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.MailingList", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_list_members_list");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Subscriber", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SubscriberId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_list_members_subscriber");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_list_members_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.MailingList", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_lists_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.SendBatch", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.SendJob", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SendJobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_batches_job");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_batches_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.SendJob", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Campaign", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CampaignId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_jobs_campaign");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.MailingList", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_jobs_list");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_jobs_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Subscriber", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_subscribers_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.WebhookNonce", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.AuthClient", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ClientId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_webhook_nonces_client");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,490 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SendEngine.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Initial : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:PostgresExtension:citext", ",,");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "auth_clients",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
client_id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
scopes = table.Column<string[]>(type: "text[]", nullable: false),
|
||||||
|
status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_auth_clients", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "tenants",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_tenants", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "auth_client_keys",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
client_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
key_hash = table.Column<string>(type: "text", nullable: false),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
revoked_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_auth_client_keys", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_auth_client_keys_client",
|
||||||
|
column: x => x.client_id,
|
||||||
|
principalTable: "auth_clients",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "webhook_nonces",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
client_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
nonce = table.Column<string>(type: "text", nullable: false),
|
||||||
|
received_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_webhook_nonces", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_webhook_nonces_client",
|
||||||
|
column: x => x.client_id,
|
||||||
|
principalTable: "auth_clients",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "events_inbox",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
event_type = table.Column<string>(type: "text", nullable: false),
|
||||||
|
source = table.Column<string>(type: "text", nullable: false),
|
||||||
|
payload = table.Column<string>(type: "jsonb", nullable: false),
|
||||||
|
received_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
processed_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
error = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_events_inbox", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_events_inbox_tenant",
|
||||||
|
column: x => x.tenant_id,
|
||||||
|
principalTable: "tenants",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "lists",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_lists", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_lists_tenant",
|
||||||
|
column: x => x.tenant_id,
|
||||||
|
principalTable: "tenants",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "subscribers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
email = table.Column<string>(type: "citext", nullable: false),
|
||||||
|
status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
preferences = table.Column<string>(type: "jsonb", nullable: true),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_subscribers", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_subscribers_tenant",
|
||||||
|
column: x => x.tenant_id,
|
||||||
|
principalTable: "tenants",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "campaigns",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
list_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
subject = table.Column<string>(type: "text", nullable: true),
|
||||||
|
body_html = table.Column<string>(type: "text", nullable: true),
|
||||||
|
body_text = table.Column<string>(type: "text", nullable: true),
|
||||||
|
template = table.Column<string>(type: "jsonb", nullable: true),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_campaigns", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_campaigns_list",
|
||||||
|
column: x => x.list_id,
|
||||||
|
principalTable: "lists",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_campaigns_tenant",
|
||||||
|
column: x => x.tenant_id,
|
||||||
|
principalTable: "tenants",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "list_members",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
list_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
subscriber_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_list_members", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_list_members_list",
|
||||||
|
column: x => x.list_id,
|
||||||
|
principalTable: "lists",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_list_members_subscriber",
|
||||||
|
column: x => x.subscriber_id,
|
||||||
|
principalTable: "subscribers",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_list_members_tenant",
|
||||||
|
column: x => x.tenant_id,
|
||||||
|
principalTable: "tenants",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "send_jobs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
list_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
campaign_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
scheduled_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
window_start = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
window_end = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_send_jobs", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_send_jobs_campaign",
|
||||||
|
column: x => x.campaign_id,
|
||||||
|
principalTable: "campaigns",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_send_jobs_list",
|
||||||
|
column: x => x.list_id,
|
||||||
|
principalTable: "lists",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_send_jobs_tenant",
|
||||||
|
column: x => x.tenant_id,
|
||||||
|
principalTable: "tenants",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "delivery_summary",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
send_job_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
total = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
delivered = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
bounced = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
complained = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_delivery_summary", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_delivery_summary_job",
|
||||||
|
column: x => x.send_job_id,
|
||||||
|
principalTable: "send_jobs",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_delivery_summary_tenant",
|
||||||
|
column: x => x.tenant_id,
|
||||||
|
principalTable: "tenants",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "send_batches",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
send_job_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
size = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_send_batches", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_send_batches_job",
|
||||||
|
column: x => x.send_job_id,
|
||||||
|
principalTable: "send_jobs",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_send_batches_tenant",
|
||||||
|
column: x => x.tenant_id,
|
||||||
|
principalTable: "tenants",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_auth_client_keys_client",
|
||||||
|
table: "auth_client_keys",
|
||||||
|
column: "client_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_auth_clients_tenant",
|
||||||
|
table: "auth_clients",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_campaigns_tenant",
|
||||||
|
table: "campaigns",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_campaigns_list_id",
|
||||||
|
table: "campaigns",
|
||||||
|
column: "list_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_delivery_summary_job",
|
||||||
|
table: "delivery_summary",
|
||||||
|
column: "send_job_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_delivery_summary_tenant_id_send_job_id",
|
||||||
|
table: "delivery_summary",
|
||||||
|
columns: new[] { "tenant_id", "send_job_id" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_events_inbox_status",
|
||||||
|
table: "events_inbox",
|
||||||
|
column: "status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_events_inbox_tenant",
|
||||||
|
table: "events_inbox",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_events_inbox_type",
|
||||||
|
table: "events_inbox",
|
||||||
|
column: "event_type");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_list_members_list",
|
||||||
|
table: "list_members",
|
||||||
|
column: "list_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_list_members_tenant",
|
||||||
|
table: "list_members",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_list_members_subscriber_id",
|
||||||
|
table: "list_members",
|
||||||
|
column: "subscriber_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_list_members_tenant_id_list_id_subscriber_id",
|
||||||
|
table: "list_members",
|
||||||
|
columns: new[] { "tenant_id", "list_id", "subscriber_id" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_lists_tenant",
|
||||||
|
table: "lists",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_send_batches_job",
|
||||||
|
table: "send_batches",
|
||||||
|
column: "send_job_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_send_batches_status",
|
||||||
|
table: "send_batches",
|
||||||
|
column: "status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_send_batches_tenant_id",
|
||||||
|
table: "send_batches",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_send_jobs_status",
|
||||||
|
table: "send_jobs",
|
||||||
|
column: "status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_send_jobs_tenant",
|
||||||
|
table: "send_jobs",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_send_jobs_campaign_id",
|
||||||
|
table: "send_jobs",
|
||||||
|
column: "campaign_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_send_jobs_list_id",
|
||||||
|
table: "send_jobs",
|
||||||
|
column: "list_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_subscribers_tenant",
|
||||||
|
table: "subscribers",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_subscribers_tenant_id_email",
|
||||||
|
table: "subscribers",
|
||||||
|
columns: new[] { "tenant_id", "email" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_webhook_nonces_client",
|
||||||
|
table: "webhook_nonces",
|
||||||
|
column: "client_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_webhook_nonces_client_id_nonce",
|
||||||
|
table: "webhook_nonces",
|
||||||
|
columns: new[] { "client_id", "nonce" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "auth_client_keys");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "delivery_summary");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "events_inbox");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "list_members");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "send_batches");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "webhook_nonces");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "subscribers");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "send_jobs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "auth_clients");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "campaigns");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "lists");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "tenants");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,683 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using SendEngine.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SendEngine.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SendEngineDbContext))]
|
||||||
|
partial class SendEngineDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.AuthClient", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string[]>("Scopes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("scopes");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_auth_clients_tenant");
|
||||||
|
|
||||||
|
b.ToTable("auth_clients", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.AuthClientKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("ClientId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("KeyHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("key_hash");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId")
|
||||||
|
.HasDatabaseName("idx_auth_client_keys_client");
|
||||||
|
|
||||||
|
b.ToTable("auth_client_keys", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Campaign", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("BodyHtml")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("body_html");
|
||||||
|
|
||||||
|
b.Property<string>("BodyText")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("body_text");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("ListId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string>("Subject")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("subject");
|
||||||
|
|
||||||
|
b.Property<string>("Template")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("template");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ListId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_campaigns_tenant");
|
||||||
|
|
||||||
|
b.ToTable("campaigns", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.DeliverySummary", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<int>("Bounced")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("bounced");
|
||||||
|
|
||||||
|
b.Property<int>("Complained")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("complained");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Delivered")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("delivered");
|
||||||
|
|
||||||
|
b.Property<Guid>("SendJobId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("send_job_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<int>("Total")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("total");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SendJobId")
|
||||||
|
.HasDatabaseName("idx_delivery_summary_job");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "SendJobId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("delivery_summary", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.EventInbox", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("Error")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("error");
|
||||||
|
|
||||||
|
b.Property<string>("EventType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("event_type");
|
||||||
|
|
||||||
|
b.Property<string>("Payload")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("payload");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ProcessedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("processed_at");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("received_at");
|
||||||
|
|
||||||
|
b.Property<string>("Source")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("source");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("EventType")
|
||||||
|
.HasDatabaseName("idx_events_inbox_type");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_events_inbox_status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_events_inbox_tenant");
|
||||||
|
|
||||||
|
b.ToTable("events_inbox", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.ListMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("ListId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("SubscriberId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("subscriber_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_list_members_list");
|
||||||
|
|
||||||
|
b.HasIndex("SubscriberId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_list_members_tenant");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "ListId", "SubscriberId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("list_members", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.MailingList", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_lists_tenant");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.SendBatch", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("SendJobId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("send_job_id");
|
||||||
|
|
||||||
|
b.Property<int>("Size")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("size");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SendJobId")
|
||||||
|
.HasDatabaseName("idx_send_batches_job");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_send_batches_status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("send_batches", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.SendJob", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("CampaignId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("campaign_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("ListId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ScheduledAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("scheduled_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("WindowEnd")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("window_end");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("WindowStart")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("window_start");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CampaignId");
|
||||||
|
|
||||||
|
b.HasIndex("ListId");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_send_jobs_status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_send_jobs_tenant");
|
||||||
|
|
||||||
|
b.ToTable("send_jobs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Subscriber", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("citext")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<string>("Preferences")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("preferences");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_subscribers_tenant");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "Email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("subscribers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Tenant", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("tenants", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.WebhookNonce", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("ClientId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<string>("Nonce")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("nonce");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReceivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("received_at");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId")
|
||||||
|
.HasDatabaseName("idx_webhook_nonces_client");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId", "Nonce")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("webhook_nonces", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.AuthClientKey", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.AuthClient", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ClientId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_auth_client_keys_client");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Campaign", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.MailingList", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_campaigns_list");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_campaigns_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.DeliverySummary", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.SendJob", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SendJobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_delivery_summary_job");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_delivery_summary_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.EventInbox", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_events_inbox_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.ListMember", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.MailingList", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_list_members_list");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Subscriber", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SubscriberId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_list_members_subscriber");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_list_members_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.MailingList", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_lists_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.SendBatch", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.SendJob", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SendJobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_batches_job");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_batches_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.SendJob", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Campaign", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CampaignId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_jobs_campaign");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.MailingList", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_jobs_list");
|
||||||
|
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_send_jobs_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.Subscriber", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.Tenant", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_subscribers_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SendEngine.Domain.Entities.WebhookNonce", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SendEngine.Domain.Entities.AuthClient", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ClientId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_webhook_nonces_client");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
219
src/SendEngine.Infrastructure/Data/SendEngineDbContext.cs
Normal file
219
src/SendEngine.Infrastructure/Data/SendEngineDbContext.cs
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SendEngine.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SendEngine.Infrastructure.Data;
|
||||||
|
|
||||||
|
public sealed class SendEngineDbContext : DbContext
|
||||||
|
{
|
||||||
|
public SendEngineDbContext(DbContextOptions<SendEngineDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<Tenant> Tenants => Set<Tenant>();
|
||||||
|
public DbSet<MailingList> Lists => Set<MailingList>();
|
||||||
|
public DbSet<Subscriber> Subscribers => Set<Subscriber>();
|
||||||
|
public DbSet<ListMember> ListMembers => Set<ListMember>();
|
||||||
|
public DbSet<EventInbox> EventsInbox => Set<EventInbox>();
|
||||||
|
public DbSet<Campaign> Campaigns => Set<Campaign>();
|
||||||
|
public DbSet<SendJob> SendJobs => Set<SendJob>();
|
||||||
|
public DbSet<SendBatch> SendBatches => Set<SendBatch>();
|
||||||
|
public DbSet<DeliverySummary> DeliverySummaries => Set<DeliverySummary>();
|
||||||
|
public DbSet<AuthClient> AuthClients => Set<AuthClient>();
|
||||||
|
public DbSet<AuthClientKey> AuthClientKeys => Set<AuthClientKey>();
|
||||||
|
public DbSet<WebhookNonce> WebhookNonces => Set<WebhookNonce>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.HasPostgresExtension("citext");
|
||||||
|
|
||||||
|
modelBuilder.Entity<Tenant>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("tenants");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.Name).HasColumnName("name");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<MailingList>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("lists");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.Name).HasColumnName("name");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.HasIndex(e => e.TenantId).HasDatabaseName("idx_lists_tenant");
|
||||||
|
entity.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).HasConstraintName("fk_lists_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Subscriber>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("subscribers");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.Email).HasColumnName("email").HasColumnType("citext");
|
||||||
|
entity.Property(e => e.Status).HasColumnName("status");
|
||||||
|
entity.Property(e => e.Preferences).HasColumnName("preferences").HasColumnType("jsonb");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||||
|
entity.HasIndex(e => e.TenantId).HasDatabaseName("idx_subscribers_tenant");
|
||||||
|
entity.HasIndex(e => new { e.TenantId, e.Email }).IsUnique();
|
||||||
|
entity.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).HasConstraintName("fk_subscribers_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ListMember>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("list_members");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.ListId).HasColumnName("list_id");
|
||||||
|
entity.Property(e => e.SubscriberId).HasColumnName("subscriber_id");
|
||||||
|
entity.Property(e => e.Status).HasColumnName("status");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||||
|
entity.HasIndex(e => e.TenantId).HasDatabaseName("idx_list_members_tenant");
|
||||||
|
entity.HasIndex(e => e.ListId).HasDatabaseName("idx_list_members_list");
|
||||||
|
entity.HasIndex(e => new { e.TenantId, e.ListId, e.SubscriberId }).IsUnique();
|
||||||
|
entity.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).HasConstraintName("fk_list_members_tenant");
|
||||||
|
entity.HasOne<MailingList>().WithMany().HasForeignKey(e => e.ListId).HasConstraintName("fk_list_members_list");
|
||||||
|
entity.HasOne<Subscriber>().WithMany().HasForeignKey(e => e.SubscriberId).HasConstraintName("fk_list_members_subscriber");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<EventInbox>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("events_inbox");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.EventType).HasColumnName("event_type");
|
||||||
|
entity.Property(e => e.Source).HasColumnName("source");
|
||||||
|
entity.Property(e => e.Payload).HasColumnName("payload").HasColumnType("jsonb");
|
||||||
|
entity.Property(e => e.ReceivedAt).HasColumnName("received_at");
|
||||||
|
entity.Property(e => e.ProcessedAt).HasColumnName("processed_at");
|
||||||
|
entity.Property(e => e.Status).HasColumnName("status");
|
||||||
|
entity.Property(e => e.Error).HasColumnName("error");
|
||||||
|
entity.HasIndex(e => e.TenantId).HasDatabaseName("idx_events_inbox_tenant");
|
||||||
|
entity.HasIndex(e => e.EventType).HasDatabaseName("idx_events_inbox_type");
|
||||||
|
entity.HasIndex(e => e.Status).HasDatabaseName("idx_events_inbox_status");
|
||||||
|
entity.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).HasConstraintName("fk_events_inbox_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Campaign>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("campaigns");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.ListId).HasColumnName("list_id");
|
||||||
|
entity.Property(e => e.Name).HasColumnName("name");
|
||||||
|
entity.Property(e => e.Subject).HasColumnName("subject");
|
||||||
|
entity.Property(e => e.BodyHtml).HasColumnName("body_html");
|
||||||
|
entity.Property(e => e.BodyText).HasColumnName("body_text");
|
||||||
|
entity.Property(e => e.Template).HasColumnName("template").HasColumnType("jsonb");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.HasIndex(e => e.TenantId).HasDatabaseName("idx_campaigns_tenant");
|
||||||
|
entity.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).HasConstraintName("fk_campaigns_tenant");
|
||||||
|
entity.HasOne<MailingList>().WithMany().HasForeignKey(e => e.ListId).HasConstraintName("fk_campaigns_list");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<SendJob>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("send_jobs");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.ListId).HasColumnName("list_id");
|
||||||
|
entity.Property(e => e.CampaignId).HasColumnName("campaign_id");
|
||||||
|
entity.Property(e => e.ScheduledAt).HasColumnName("scheduled_at");
|
||||||
|
entity.Property(e => e.WindowStart).HasColumnName("window_start");
|
||||||
|
entity.Property(e => e.WindowEnd).HasColumnName("window_end");
|
||||||
|
entity.Property(e => e.Status).HasColumnName("status");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||||
|
entity.HasIndex(e => e.TenantId).HasDatabaseName("idx_send_jobs_tenant");
|
||||||
|
entity.HasIndex(e => e.Status).HasDatabaseName("idx_send_jobs_status");
|
||||||
|
entity.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).HasConstraintName("fk_send_jobs_tenant");
|
||||||
|
entity.HasOne<MailingList>().WithMany().HasForeignKey(e => e.ListId).HasConstraintName("fk_send_jobs_list");
|
||||||
|
entity.HasOne<Campaign>().WithMany().HasForeignKey(e => e.CampaignId).HasConstraintName("fk_send_jobs_campaign");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<SendBatch>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("send_batches");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.SendJobId).HasColumnName("send_job_id");
|
||||||
|
entity.Property(e => e.Status).HasColumnName("status");
|
||||||
|
entity.Property(e => e.Size).HasColumnName("size");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||||
|
entity.HasIndex(e => e.SendJobId).HasDatabaseName("idx_send_batches_job");
|
||||||
|
entity.HasIndex(e => e.Status).HasDatabaseName("idx_send_batches_status");
|
||||||
|
entity.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).HasConstraintName("fk_send_batches_tenant");
|
||||||
|
entity.HasOne<SendJob>().WithMany().HasForeignKey(e => e.SendJobId).HasConstraintName("fk_send_batches_job");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<DeliverySummary>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("delivery_summary");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.SendJobId).HasColumnName("send_job_id");
|
||||||
|
entity.Property(e => e.Total).HasColumnName("total");
|
||||||
|
entity.Property(e => e.Delivered).HasColumnName("delivered");
|
||||||
|
entity.Property(e => e.Bounced).HasColumnName("bounced");
|
||||||
|
entity.Property(e => e.Complained).HasColumnName("complained");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||||
|
entity.HasIndex(e => e.SendJobId).HasDatabaseName("idx_delivery_summary_job");
|
||||||
|
entity.HasIndex(e => new { e.TenantId, e.SendJobId }).IsUnique();
|
||||||
|
entity.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).HasConstraintName("fk_delivery_summary_tenant");
|
||||||
|
entity.HasOne<SendJob>().WithMany().HasForeignKey(e => e.SendJobId).HasConstraintName("fk_delivery_summary_job");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<AuthClient>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("auth_clients");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||||
|
entity.Property(e => e.ClientId).HasColumnName("client_id");
|
||||||
|
entity.Property(e => e.Name).HasColumnName("name");
|
||||||
|
entity.Property(e => e.Scopes).HasColumnName("scopes");
|
||||||
|
entity.Property(e => e.Status).HasColumnName("status");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.HasIndex(e => e.TenantId).HasDatabaseName("idx_auth_clients_tenant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<AuthClientKey>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("auth_client_keys");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.ClientId).HasColumnName("client_id");
|
||||||
|
entity.Property(e => e.KeyHash).HasColumnName("key_hash");
|
||||||
|
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||||
|
entity.Property(e => e.RevokedAt).HasColumnName("revoked_at");
|
||||||
|
entity.HasIndex(e => e.ClientId).HasDatabaseName("idx_auth_client_keys_client");
|
||||||
|
entity.HasOne<AuthClient>().WithMany().HasForeignKey(e => e.ClientId).HasConstraintName("fk_auth_client_keys_client");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<WebhookNonce>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("webhook_nonces");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
|
entity.Property(e => e.ClientId).HasColumnName("client_id");
|
||||||
|
entity.Property(e => e.Nonce).HasColumnName("nonce");
|
||||||
|
entity.Property(e => e.ReceivedAt).HasColumnName("received_at");
|
||||||
|
entity.HasIndex(e => e.ClientId).HasDatabaseName("idx_webhook_nonces_client");
|
||||||
|
entity.HasIndex(e => new { e.ClientId, e.Nonce }).IsUnique();
|
||||||
|
entity.HasOne<AuthClient>().WithMany().HasForeignKey(e => e.ClientId).HasConstraintName("fk_webhook_nonces_client");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace SendEngine.Infrastructure.Data;
|
||||||
|
|
||||||
|
public sealed class SendEngineDbContextFactory : IDesignTimeDbContextFactory<SendEngineDbContext>
|
||||||
|
{
|
||||||
|
public SendEngineDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var connectionString = configuration.GetConnectionString("Default")
|
||||||
|
?? configuration["ConnectionStrings__Default"]
|
||||||
|
?? "Host=localhost;Database=send_engine;Username=postgres;Password=postgres";
|
||||||
|
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<SendEngineDbContext>();
|
||||||
|
optionsBuilder.UseNpgsql(connectionString);
|
||||||
|
|
||||||
|
return new SendEngineDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/SendEngine.Infrastructure/DependencyInjection.cs
Normal file
21
src/SendEngine.Infrastructure/DependencyInjection.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SendEngine.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace SendEngine.Infrastructure;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var connectionString = configuration.GetConnectionString("Default")
|
||||||
|
?? configuration["ConnectionStrings__Default"]
|
||||||
|
?? "Host=localhost;Database=send_engine;Username=postgres;Password=postgres";
|
||||||
|
|
||||||
|
services.AddDbContext<SendEngineDbContext>(options =>
|
||||||
|
options.UseNpgsql(connectionString));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SendEngine.Domain\SendEngine.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\SendEngine.Application\SendEngine.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
35
src/SendEngine.Installer/Program.cs
Normal file
35
src/SendEngine.Installer/Program.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SendEngine.Infrastructure;
|
||||||
|
using SendEngine.Infrastructure.Data;
|
||||||
|
|
||||||
|
var command = args.Length > 0 ? args[0] : "migrate";
|
||||||
|
|
||||||
|
if (command is "-h" or "--help" or "help")
|
||||||
|
{
|
||||||
|
Console.WriteLine("SendEngine Installer");
|
||||||
|
Console.WriteLine("Usage:");
|
||||||
|
Console.WriteLine(" dotnet run --project src/SendEngine.Installer -- migrate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddInfrastructure(configuration);
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
if (command == "migrate")
|
||||||
|
{
|
||||||
|
using var scope = provider.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<SendEngineDbContext>();
|
||||||
|
db.Database.Migrate();
|
||||||
|
Console.WriteLine("Database migration completed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Unknown command: {command}");
|
||||||
20
src/SendEngine.Installer/SendEngine.Installer.csproj
Normal file
20
src/SendEngine.Installer/SendEngine.Installer.csproj
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SendEngine.Infrastructure\SendEngine.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
x
Reference in New Issue
Block a user