Compare commits

..

3 Commits

35 changed files with 3264 additions and 11 deletions

View File

@@ -213,17 +213,18 @@
}, },
"active": "41ed24ce492fd06e", "active": "41ed24ce492fd06e",
"lastOpenFiles": [ "lastOpenFiles": [
"src/Frontend/src/components/common/StatusChip.tsx", "src/Database/Services/PasswordHasher.cs",
"src/Frontend/src/components/common/ProgressBar.tsx", "src/Database/Services/DatabaseSeeder.cs",
"src/Frontend/src/components/common/LoadingSpinner.tsx", "src/Database/Services/DatabaseMigrator.cs",
"src/Frontend/src/components/common/ErrorDisplay.tsx", "src/Database/Repositories/IUserRepository.cs",
"src/Frontend/src/components/Publishing/PublishingDashboard.tsx", "src/Database/Repositories/IPublicationRepository.cs",
"src/Frontend/src/components/Packages/PackageList.tsx", "src/Database/Repositories/IProjectRepository.cs",
"src/Frontend/src/components/Packages/PackageForm.tsx", "src/Database/Repositories/IPackageRepository.cs",
"src/Frontend/src/components/Layout/AppLayout.tsx", "src/Database/Repositories/IFogBugzRepository.cs",
"src/Frontend/src/types/index.ts", "src/Database/Repositories/IBuildRepository.cs",
"src/Frontend/src/services/signalRService.ts", "src/Database/Models/User.cs",
"src/Frontend/src/services/publicationService.ts", "src/Database/Models/StorageProvider.cs",
"README-Database.md",
"src/Frontend/README.md", "src/Frontend/README.md",
"legacy.md", "legacy.md",
"claude.md", "claude.md",

441
README-Database.md Normal file
View File

@@ -0,0 +1,441 @@
# Database Implementation
This document describes the PostgreSQL database implementation for the Software Release Management Platform.
## Architecture Overview
The database layer uses **Entity Framework Core 8** with **PostgreSQL 15** and includes:
- **Soft Delete Pattern**: All entities support soft deletes with audit trail
- **Repository Pattern**: Clean abstraction layer for data access
- **Database Constraints**: Enforced data integrity at the database level
- **JSON Support**: PostgreSQL JSONB columns for flexible configuration storage
- **Migration Support**: Code-first database schema management
- **Connection Pooling**: Optimized for high-performance applications
## Entity Relationships
```mermaid
erDiagram
Users {
int Id PK
string Username UK
string PasswordHash
string Role
datetime CreatedAt
datetime LastLogin
bool IsDeleted
}
Projects {
int Id PK
string Name
string CCNetProjectName UK
string Status
datetime CreatedAt
bool IsDeleted
}
Builds {
int Id PK
int ProjectId FK
string BuildNumber
string Status
datetime StartTime
datetime EndTime
bool IsDeleted
}
Packages {
int Id PK
string Title
string Version
int ProjectId FK
int SourceBuildId FK
string Status
datetime PublishDate
bool IsDeleted
}
PackageConfigurations {
int Id PK
int PackageId FK
string BuildFolder
bool ZipContents
jsonb StorageSettings
jsonb HelpCenterSettings
bool IsDeleted
}
Publications {
int Id PK
int PackageId FK
string Status
datetime PublishedAt
jsonb PublicationDetails
bool IsDeleted
}
PublishingSteps {
int Id PK
int PublicationId FK
string StepName
string Status
int ProgressPercent
datetime StartedAt
datetime CompletedAt
bool IsDeleted
}
FogBugzCases {
int Id PK
int CaseId UK
string Title
string Status
datetime LastUpdated
bool IsOpen
bool IsDeleted
}
FogBugzEvents {
int Id PK
int CaseId FK
string EventType
string User
datetime EventDate
string ReleaseNote
int ZendeskNumber
bool IsDeleted
}
BuildCommits {
int Id PK
int BuildId FK
string CommitHash
string FogBugzCaseId
jsonb ModifiedFiles
datetime CommitDate
bool IsDeleted
}
StorageProviders {
int Id PK
string Name UK
string Type
jsonb Configuration
bool IsActive
bool IsDeleted
}
HelpCenterProviders {
int Id PK
string Name UK
string Type
jsonb Configuration
bool IsActive
bool IsDeleted
}
Projects ||--o{ Builds : "produces"
Projects ||--o{ Packages : "contains"
Builds ||--o{ BuildCommits : "includes"
Packages ||--o{ PackageConfigurations : "configured by"
Packages ||--o{ Publications : "published as"
Publications ||--o{ PublishingSteps : "tracked by"
FogBugzCases ||--o{ FogBugzEvents : "has history"
BuildCommits }o--|| FogBugzCases : "references"
```
## Database Schema
### Core Tables
#### Users
- **Purpose**: User authentication and authorization
- **Key Features**: Role-based access control, password hashing, audit trail
#### Projects
- **Purpose**: CruiseControl.NET project integration
- **Key Features**: Maps to CC.NET project names, tracks build history
#### Builds
- **Purpose**: Individual build records from CC.NET
- **Key Features**: Build status tracking, timing information, artifact paths
#### BuildCommits
- **Purpose**: Source code commits within builds
- **Key Features**: FogBugz case linking, modified files (JSON), commit metadata
### Package Management
#### Packages
- **Purpose**: User-defined software release configurations
- **Key Features**: Version tracking, publishing status, project association
#### PackageConfigurations
- **Purpose**: Package-specific publishing settings
- **Key Features**: Storage settings (JSON), help center settings (JSON), build options
### Publishing Workflow
#### Publications
- **Purpose**: Publishing execution records
- **Key Features**: Workflow status, publication details (JSON), timing
#### PublishingSteps
- **Purpose**: Individual workflow step tracking
- **Key Features**: Progress percentage, error handling, step ordering
### External Integration
#### FogBugzCases & FogBugzEvents
- **Purpose**: FogBugz/Manuscript integration
- **Key Features**: Case history, release note extraction, Zendesk linking
#### StorageProviders & HelpCenterProviders
- **Purpose**: External service configuration
- **Key Features**: Pluggable providers, encrypted configuration (JSON)
## Soft Delete Implementation
All entities inherit from `BaseEntity` with soft delete support:
```csharp
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
public string? CreatedBy { get; set; }
public string? UpdatedBy { get; set; }
}
```
### Query Filters
Entity Framework automatically filters out soft-deleted records:
```csharp
modelBuilder.Entity<User>().HasQueryFilter(e => !e.IsDeleted);
```
### Manual Override
To include soft-deleted records:
```csharp
var allUsers = context.Users.IgnoreQueryFilters().Where(u => u.IsDeleted);
```
## Repository Pattern
### Interface Example
```csharp
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id);
Task<User?> GetByUsernameAsync(string username);
Task<List<User>> GetAllAsync();
Task<User> CreateAsync(User user);
Task<User> UpdateAsync(User user);
Task DeleteAsync(int id, string deletedBy);
Task<bool> ExistsAsync(int id);
}
```
### Benefits
- Clean separation of concerns
- Testable data access layer
- Consistent error handling
- Transaction management
## Database Configuration
### Connection String
Production connection strings stored in `secrets.yaml`:
```yaml
Database:
ConnectionString: "Host=localhost;Port=5432;Database=software_release_management;Username=postgres;Password=your_secure_password"
```
### Performance Settings
```json
{
"Database": {
"CommandTimeout": 30,
"Pooling": {
"MinPoolSize": 5,
"MaxPoolSize": 100,
"ConnectionIdleLifetime": 300
},
"Retry": {
"EnableRetryOnFailure": true,
"MaxRetryCount": 5,
"MaxRetryDelay": 30
}
}
}
```
## Setup Instructions
### 1. Prerequisites
- Docker and Docker Compose
- .NET 8 SDK
- PostgreSQL client tools (optional)
### 2. Start Database
```bash
# Start PostgreSQL with Docker Compose
docker compose up postgres -d
# Optional: Start with PgAdmin
docker compose --profile dev up -d
```
### 3. Run Migrations
```bash
cd src/Database
dotnet ef migrations add InitialCreate
dotnet ef database update
```
### 4. Seed Data
```bash
# Development environment automatically seeds test data
ASPNETCORE_ENVIRONMENT=Development dotnet run
```
## Development Workflow
### Creating Migrations
```bash
# Navigate to Database project
cd src/Database
# Create new migration
dotnet ef migrations add MigrationName
# Review generated migration files
# Apply migration
dotnet ef database update
```
### Database Scripts
```bash
# Generate SQL script for production deployment
dotnet ef migrations script --output migrations.sql
# Generate script for specific migration range
dotnet ef migrations script PreviousMigration TargetMigration
```
### Data Seeding
The database automatically seeds:
- Default users (admin, user, readonly)
- Storage providers (AWS S3, Box.com, FTP)
- Help center providers (Zendesk, Salesforce)
- Test projects and packages (development only)
## Security Features
### Password Hashing
- BCrypt with work factor 12
- Automatic salt generation
- Secure password verification
### Secrets Management
- Production secrets in `secrets.yaml`
- Environment variable overrides
- Docker secrets support
### Database Security
- Connection pooling limits
- SQL injection protection (parameterized queries)
- Role-based access control
- Audit logging
## Performance Considerations
### Indexing Strategy
- Primary keys (clustered indexes)
- Foreign key relationships
- Query optimization indexes:
- `Users.Username` (unique)
- `Projects.CCNetProjectName` (unique)
- `Builds.StartTime`, `Builds.Status`
- `FogBugzCases.LastUpdated`
- `Publications.Status`, `Publications.PublishedAt`
### JSON Column Usage
PostgreSQL JSONB columns provide:
- Efficient storage and querying
- Flexible configuration schemas
- GIN index support for complex queries
### Connection Pooling
- Configured connection limits
- Idle connection cleanup
- Health check monitoring
## Monitoring and Maintenance
### Health Checks
```csharp
services.AddHealthChecks()
.AddDbContextCheck<DatabaseContext>("database");
```
### Logging
- Entity Framework query logging
- Migration execution logs
- Performance monitoring
- Error tracking
### Backup Strategy
```bash
# Database backup
pg_dump -h localhost -p 5432 -U postgres -d software_release_management > backup.sql
# Restore from backup
psql -h localhost -p 5432 -U postgres -d software_release_management < backup.sql
```
## Troubleshooting
### Common Issues
1. **Connection Issues**
```bash
# Check PostgreSQL status
docker compose logs postgres
# Test connection
psql -h localhost -p 5432 -U postgres -d software_release_management
```
2. **Migration Failures**
```bash
# Reset migrations (development only)
dotnet ef database drop --force
dotnet ef database update
```
3. **Performance Issues**
```sql
-- Check slow queries
SELECT query, mean_time, calls
FROM pg_stat_statements
ORDER BY mean_time DESC LIMIT 10;
```
### Support
- Check Entity Framework documentation
- Review PostgreSQL logs
- Monitor application performance
- Use PgAdmin for database administration
## Next Steps
- Set up monitoring and alerting
- Implement database backup automation
- Configure read replicas for scaling
- Set up connection string encryption

73
docker-compose.yml Normal file
View File

@@ -0,0 +1,73 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: srm_postgres
environment:
POSTGRES_DB: software_release_management
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/pgdata
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/01-init-db.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d software_release_management"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- srm-network
# Database migration service
database-migrator:
build:
context: ./src/Database
dockerfile: Dockerfile.migrator
container_name: srm_db_migrator
depends_on:
postgres:
condition: service_healthy
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=software_release_management;Username=postgres;Password=postgres
networks:
- srm-network
restart: no
# PgAdmin for database management (optional)
pgadmin:
image: dpage/pgadmin4:latest
container_name: srm_pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: admin@company.com
PGADMIN_DEFAULT_PASSWORD: admin123
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
ports:
- "5050:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
- ./scripts/servers.json:/pgadmin4/servers.json:ro
depends_on:
- postgres
networks:
- srm-network
profiles:
- dev
restart: unless-stopped
volumes:
postgres_data:
driver: local
pgadmin_data:
driver: local
networks:
srm-network:
driver: bridge
name: software-release-management

30
scripts/init-db.sql Normal file
View File

@@ -0,0 +1,30 @@
-- Initialize PostgreSQL database for Software Release Management
-- This script runs during container initialization
-- Create additional users if needed
-- CREATE USER srm_app WITH PASSWORD 'secure_password';
-- GRANT CONNECT ON DATABASE software_release_management TO srm_app;
-- Enable UUID extension if needed in the future
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Enable PostGIS extension if geographical features are needed
-- CREATE EXTENSION IF NOT EXISTS postgis;
-- Set timezone
SET timezone = 'UTC';
-- Create schema for application (optional, using public by default)
-- CREATE SCHEMA IF NOT EXISTS srm;
-- Grant permissions
-- GRANT USAGE ON SCHEMA public TO srm_app;
-- GRANT CREATE ON SCHEMA public TO srm_app;
-- Log initialization
DO $$
BEGIN
RAISE NOTICE 'Database initialized for Software Release Management';
RAISE NOTICE 'Version: PostgreSQL %', version();
RAISE NOTICE 'Current time: %', NOW();
END $$;

23
scripts/servers.json Normal file
View File

@@ -0,0 +1,23 @@
{
"Servers": {
"1": {
"Name": "Software Release Management",
"Group": "Servers",
"Host": "postgres",
"Port": 5432,
"MaintenanceDB": "postgres",
"Username": "postgres",
"PassFile": "/tmp/pgpassfile",
"SSLMode": "prefer",
"SSLCert": "<STORAGE_DIR>/.postgresql/postgresql.crt",
"SSLKey": "<STORAGE_DIR>/.postgresql/postgresql.key",
"SSLCompression": 0,
"Timeout": 10,
"UseSSHTunnel": 0,
"TunnelHost": "",
"TunnelPort": "22",
"TunnelUsername": "",
"TunnelAuthentication": 0
}
}
}

59
secrets.yaml Normal file
View File

@@ -0,0 +1,59 @@
# Database Secrets Configuration
# This file contains sensitive configuration data and should be secured
# Use environment variables or secure key vaults in production
Database:
ConnectionString: "Host=localhost;Port=5432;Database=software_release_management;Username=postgres;Password=your_secure_password"
# External Service Credentials
StorageProviders:
AWS:
AccessKey: "YOUR_AWS_ACCESS_KEY"
SecretKey: "YOUR_AWS_SECRET_KEY"
Box:
ClientId: "YOUR_BOX_CLIENT_ID"
ClientSecret: "YOUR_BOX_CLIENT_SECRET"
AccessToken: "YOUR_BOX_ACCESS_TOKEN"
RefreshToken: "YOUR_BOX_REFRESH_TOKEN"
FTP:
Username: "YOUR_FTP_USERNAME"
Password: "YOUR_FTP_PASSWORD"
HelpCenterProviders:
Zendesk:
Username: "YOUR_ZENDESK_USERNAME"
ApiKey: "YOUR_ZENDESK_API_KEY"
Salesforce:
Username: "YOUR_SALESFORCE_USERNAME"
Password: "YOUR_SALESFORCE_PASSWORD"
ClientId: "YOUR_SALESFORCE_CLIENT_ID"
ClientSecret: "YOUR_SALESFORCE_CLIENT_SECRET"
SecurityToken: "YOUR_SALESFORCE_SECURITY_TOKEN"
# External Integration Services
ExternalServices:
CruiseControl:
ServerAddress: "ccnet.company.local"
Port: 21234
FogBugz:
ServerAddress: "fogbugz.company.local"
ApiEndpoint: "/api.asp"
AuthToken: "YOUR_FOGBUGZ_TOKEN"
# JWT Authentication
Authentication:
SecretKey: "YOUR_JWT_SECRET_KEY_AT_LEAST_32_CHARACTERS_LONG"
Issuer: "SoftwareReleaseManagement"
Audience: "SRM-API"
ExpirationMinutes: 60
RefreshTokenExpirationDays: 30
# Application Settings
Application:
DefaultAdminUsername: "admin"
DefaultAdminPassword: "ChangeThisPassword123!"
DefaultAdminEmail: "admin@company.com"

View File

@@ -0,0 +1,47 @@
namespace Database.Configuration
{
public class DatabaseConfiguration
{
public const string SectionName = "Database";
public string ConnectionString { get; set; } = string.Empty;
public bool EnableSensitiveDataLogging { get; set; } = false;
public bool EnableDetailedErrors { get; set; } = false;
public int CommandTimeout { get; set; } = 30;
public bool AutoMigrate { get; set; } = false;
public bool SeedTestData { get; set; } = false;
public string Environment { get; set; } = "Development";
public PoolingOptions Pooling { get; set; } = new();
public RetryOptions Retry { get; set; } = new();
}
public class PoolingOptions
{
public int MinPoolSize { get; set; } = 5;
public int MaxPoolSize { get; set; } = 100;
public int ConnectionIdleLifetime { get; set; } = 300; // seconds
public int ConnectionPruningInterval { get; set; } = 10; // seconds
}
public class RetryOptions
{
public bool EnableRetryOnFailure { get; set; } = true;
public int MaxRetryCount { get; set; } = 5;
public int MaxRetryDelay { get; set; } = 30; // seconds
public List<string> ErrorNumbersToAdd { get; set; } = new();
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,360 @@
using Microsoft.EntityFrameworkCore;
using Database.Models;
namespace Database
{
public class DatabaseContext : DbContext
{
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
}
// Core Entities
public DbSet<User> Users { get; set; }
public DbSet<Package> Packages { get; set; }
public DbSet<PackageConfiguration> PackageConfigurations { get; set; }
public DbSet<Project> Projects { get; set; }
public DbSet<Build> Builds { get; set; }
public DbSet<BuildCommit> BuildCommits { get; set; }
// FogBugz Integration
public DbSet<FogBugzCase> FogBugzCases { get; set; }
public DbSet<FogBugzEvent> FogBugzEvents { get; set; }
// Publishing Workflow
public DbSet<Publication> Publications { get; set; }
public DbSet<PublishingStep> PublishingSteps { get; set; }
// Provider Configurations
public DbSet<StorageProvider> StorageProviders { get; set; }
public DbSet<HelpCenterProvider> HelpCenterProviders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure soft delete filter
modelBuilder.Entity<User>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<Package>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<PackageConfiguration>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<Project>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<Build>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<BuildCommit>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<FogBugzCase>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<FogBugzEvent>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<Publication>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<PublishingStep>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<StorageProvider>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<HelpCenterProvider>().HasQueryFilter(e => !e.IsDeleted);
// Configure relationships
ConfigureUserRelationships(modelBuilder);
ConfigurePackageRelationships(modelBuilder);
ConfigureProjectRelationships(modelBuilder);
ConfigureBuildRelationships(modelBuilder);
ConfigureFogBugzRelationships(modelBuilder);
ConfigurePublishingRelationships(modelBuilder);
ConfigureProviderRelationships(modelBuilder);
// Configure indexes
ConfigureIndexes(modelBuilder);
// Configure database constraints
ConfigureConstraints(modelBuilder);
}
private static void ConfigureUserRelationships(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasIndex(u => u.Username)
.IsUnique();
}
private static void ConfigurePackageRelationships(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Package>()
.HasOne(p => p.Project)
.WithMany(pr => pr.Packages)
.HasForeignKey(p => p.ProjectId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Package>()
.HasOne(p => p.SourceBuild)
.WithMany()
.HasForeignKey(p => p.SourceBuildId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<PackageConfiguration>()
.HasOne(pc => pc.Package)
.WithOne(p => p.Configuration)
.HasForeignKey<PackageConfiguration>(pc => pc.PackageId)
.OnDelete(DeleteBehavior.Cascade);
}
private static void ConfigureProjectRelationships(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Project>()
.HasIndex(p => p.CCNetProjectName)
.IsUnique();
}
private static void ConfigureBuildRelationships(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Build>()
.HasOne(b => b.Project)
.WithMany(p => p.Builds)
.HasForeignKey(b => b.ProjectId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Build>()
.HasIndex(b => new { b.ProjectId, b.BuildNumber })
.IsUnique();
modelBuilder.Entity<BuildCommit>()
.HasOne(bc => bc.Build)
.WithMany(b => b.Commits)
.HasForeignKey(bc => bc.BuildId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<BuildCommit>()
.HasOne(bc => bc.FogBugzCase)
.WithMany()
.HasForeignKey(bc => bc.FogBugzCaseId)
.OnDelete(DeleteBehavior.SetNull);
}
private static void ConfigureFogBugzRelationships(ModelBuilder modelBuilder)
{
modelBuilder.Entity<FogBugzCase>()
.HasIndex(c => c.CaseId)
.IsUnique();
modelBuilder.Entity<FogBugzEvent>()
.HasOne(e => e.Case)
.WithMany(c => c.Events)
.HasForeignKey(e => e.CaseId)
.OnDelete(DeleteBehavior.Cascade);
}
private static void ConfigurePublishingRelationships(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Publication>()
.HasOne(pub => pub.Package)
.WithMany(p => p.Publications)
.HasForeignKey(pub => pub.PackageId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<PublishingStep>()
.HasOne(ps => ps.Publication)
.WithMany(p => p.Steps)
.HasForeignKey(ps => ps.PublicationId)
.OnDelete(DeleteBehavior.Cascade);
}
private static void ConfigureProviderRelationships(ModelBuilder modelBuilder)
{
modelBuilder.Entity<StorageProvider>()
.HasIndex(sp => sp.Name)
.IsUnique();
modelBuilder.Entity<HelpCenterProvider>()
.HasIndex(hcp => hcp.Name)
.IsUnique();
}
private static void ConfigureIndexes(ModelBuilder modelBuilder)
{
// Performance indexes
modelBuilder.Entity<Build>()
.HasIndex(b => b.Status);
modelBuilder.Entity<Build>()
.HasIndex(b => b.StartTime);
modelBuilder.Entity<BuildCommit>()
.HasIndex(bc => bc.FogBugzCaseId);
modelBuilder.Entity<BuildCommit>()
.HasIndex(bc => bc.CommitDate);
modelBuilder.Entity<FogBugzCase>()
.HasIndex(c => c.Status);
modelBuilder.Entity<FogBugzCase>()
.HasIndex(c => c.LastUpdated);
modelBuilder.Entity<FogBugzEvent>()
.HasIndex(e => e.EventDate);
modelBuilder.Entity<Publication>()
.HasIndex(p => p.Status);
modelBuilder.Entity<Publication>()
.HasIndex(p => p.PublishedAt);
modelBuilder.Entity<PublishingStep>()
.HasIndex(ps => ps.Status);
}
private static void ConfigureConstraints(ModelBuilder modelBuilder)
{
// String length constraints
modelBuilder.Entity<User>()
.Property(u => u.Username)
.HasMaxLength(100)
.IsRequired();
modelBuilder.Entity<User>()
.Property(u => u.Role)
.HasMaxLength(50)
.IsRequired();
modelBuilder.Entity<Package>()
.Property(p => p.Title)
.HasMaxLength(200)
.IsRequired();
modelBuilder.Entity<Package>()
.Property(p => p.Version)
.HasMaxLength(50)
.IsRequired();
modelBuilder.Entity<Package>()
.Property(p => p.Status)
.HasMaxLength(50)
.IsRequired();
modelBuilder.Entity<Project>()
.Property(p => p.Name)
.HasMaxLength(200)
.IsRequired();
modelBuilder.Entity<Project>()
.Property(p => p.CCNetProjectName)
.HasMaxLength(200)
.IsRequired();
modelBuilder.Entity<Build>()
.Property(b => b.BuildNumber)
.HasMaxLength(100)
.IsRequired();
modelBuilder.Entity<Build>()
.Property(b => b.Status)
.HasMaxLength(50)
.IsRequired();
modelBuilder.Entity<BuildCommit>()
.Property(bc => bc.CommitHash)
.HasMaxLength(40);
modelBuilder.Entity<BuildCommit>()
.Property(bc => bc.User)
.HasMaxLength(100);
modelBuilder.Entity<FogBugzCase>()
.Property(c => c.Title)
.HasMaxLength(500)
.IsRequired();
modelBuilder.Entity<FogBugzCase>()
.Property(c => c.Status)
.HasMaxLength(50)
.IsRequired();
modelBuilder.Entity<FogBugzEvent>()
.Property(e => e.EventType)
.HasMaxLength(100)
.IsRequired();
modelBuilder.Entity<FogBugzEvent>()
.Property(e => e.User)
.HasMaxLength(100);
modelBuilder.Entity<Publication>()
.Property(p => p.Status)
.HasMaxLength(50)
.IsRequired();
modelBuilder.Entity<PublishingStep>()
.Property(ps => ps.StepName)
.HasMaxLength(200)
.IsRequired();
modelBuilder.Entity<PublishingStep>()
.Property(ps => ps.Status)
.HasMaxLength(50)
.IsRequired();
modelBuilder.Entity<StorageProvider>()
.Property(sp => sp.Name)
.HasMaxLength(200)
.IsRequired();
modelBuilder.Entity<StorageProvider>()
.Property(sp => sp.Type)
.HasMaxLength(50)
.IsRequired();
modelBuilder.Entity<HelpCenterProvider>()
.Property(hcp => hcp.Name)
.HasMaxLength(200)
.IsRequired();
modelBuilder.Entity<HelpCenterProvider>()
.Property(hcp => hcp.Type)
.HasMaxLength(50)
.IsRequired();
// JSON column configuration
modelBuilder.Entity<PackageConfiguration>()
.Property(pc => pc.StorageSettings)
.HasColumnType("jsonb");
modelBuilder.Entity<PackageConfiguration>()
.Property(pc => pc.HelpCenterSettings)
.HasColumnType("jsonb");
modelBuilder.Entity<BuildCommit>()
.Property(bc => bc.ModifiedFiles)
.HasColumnType("jsonb");
modelBuilder.Entity<Publication>()
.Property(p => p.PublicationDetails)
.HasColumnType("jsonb");
modelBuilder.Entity<StorageProvider>()
.Property(sp => sp.Configuration)
.HasColumnType("jsonb");
modelBuilder.Entity<HelpCenterProvider>()
.Property(hcp => hcp.Configuration)
.HasColumnType("jsonb");
// Check constraints
modelBuilder.Entity<PublishingStep>()
.HasCheckConstraint("CK_PublishingStep_ProgressPercent",
"\"ProgressPercent\" >= 0 AND \"ProgressPercent\" <= 100");
modelBuilder.Entity<User>()
.HasCheckConstraint("CK_User_Role",
"\"Role\" IN ('Admin', 'User', 'ReadOnly')");
modelBuilder.Entity<Package>()
.HasCheckConstraint("CK_Package_Status",
"\"Status\" IN ('Draft', 'Ready', 'Publishing', 'Published', 'Failed')");
modelBuilder.Entity<Build>()
.HasCheckConstraint("CK_Build_Status",
"\"Status\" IN ('Success', 'Failure', 'Exception', 'Cancelled', 'Unknown')");
modelBuilder.Entity<Publication>()
.HasCheckConstraint("CK_Publication_Status",
"\"Status\" IN ('Pending', 'InProgress', 'Completed', 'Failed', 'Cancelled')");
modelBuilder.Entity<PublishingStep>()
.HasCheckConstraint("CK_PublishingStep_Status",
"\"Status\" IN ('Pending', 'InProgress', 'Completed', 'Failed', 'Skipped')");
}
}
}

View File

@@ -0,0 +1,68 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Database.Configuration;
using Database.Services;
namespace Database.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration)
{
// Bind database configuration
var dbConfig = new DatabaseConfiguration();
configuration.GetSection(DatabaseConfiguration.SectionName).Bind(dbConfig);
services.AddSingleton(dbConfig);
// Add Entity Framework DbContext
services.AddDbContext<DatabaseContext>(options =>
{
options.UseNpgsql(dbConfig.ConnectionString, npgsqlOptions =>
{
npgsqlOptions.CommandTimeout(dbConfig.CommandTimeout);
npgsqlOptions.EnableRetryOnFailure(
maxRetryCount: dbConfig.Retry.MaxRetryCount,
maxRetryDelay: TimeSpan.FromSeconds(dbConfig.Retry.MaxRetryDelay),
errorCodesToAdd: null);
});
if (dbConfig.EnableSensitiveDataLogging)
{
options.EnableSensitiveDataLogging();
}
if (dbConfig.EnableDetailedErrors)
{
options.EnableDetailedErrors();
}
// Configure connection pooling
options.UseNpgsql(dbConfig.ConnectionString, npgsqlOptions =>
{
npgsqlOptions.CommandTimeout(dbConfig.CommandTimeout);
});
});
// Add database services
services.AddScoped<IDatabaseSeeder, DatabaseSeeder>();
services.AddScoped<IDatabaseMigrator, DatabaseMigrator>();
services.AddScoped<IPasswordHasher, PasswordHasher>();
return services;
}
public static IServiceCollection AddDatabaseRepositories(this IServiceCollection services)
{
// Add repository pattern services if needed
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IPackageRepository, PackageRepository>();
services.AddScoped<IProjectRepository, ProjectRepository>();
services.AddScoped<IBuildRepository, BuildRepository>();
services.AddScoped<IFogBugzRepository, FogBugzRepository>();
services.AddScoped<IPublicationRepository, PublicationRepository>();
return services;
}
}
}

View File

@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
namespace Database.Models
{
public abstract class BaseEntity
{
[Key]
public int Id { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public bool IsDeleted { get; set; } = false;
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
public string? CreatedBy { get; set; }
public string? UpdatedBy { get; set; }
public void MarkAsDeleted(string deletedBy)
{
IsDeleted = true;
DeletedAt = DateTime.UtcNow;
DeletedBy = deletedBy;
}
public void UpdateTimestamp(string updatedBy)
{
UpdatedAt = DateTime.UtcNow;
UpdatedBy = updatedBy;
}
}
}

View File

@@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Database.Models
{
public class Build : BaseEntity
{
public int ProjectId { get; set; }
[Required]
[MaxLength(100)]
public string BuildNumber { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Status { get; set; } = "Unknown"; // Success, Failure, Exception, Cancelled, Unknown
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public string? LogPath { get; set; }
public string? ArtifactPath { get; set; }
public TimeSpan? Duration => EndTime?.Subtract(StartTime);
// Navigation properties
[ForeignKey(nameof(ProjectId))]
public Project Project { get; set; } = null!;
public ICollection<BuildCommit> Commits { get; set; } = new List<BuildCommit>();
}
}

View File

@@ -0,0 +1,60 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace Database.Models
{
public class BuildCommit : BaseEntity
{
public int BuildId { get; set; }
[MaxLength(40)]
public string? CommitHash { get; set; }
public string? Comment { get; set; }
[MaxLength(100)]
public string? User { get; set; }
public DateTime CommitDate { get; set; }
[MaxLength(20)]
public string? FogBugzCaseId { get; set; }
public int? FogBugzCaseIdInt => int.TryParse(FogBugzCaseId, out int result) ? result : null;
// JSON column for PostgreSQL
[Column(TypeName = "jsonb")]
public string? ModifiedFiles { get; set; }
public string? ReleaseNote { get; set; }
// Navigation properties
[ForeignKey(nameof(BuildId))]
public Build Build { get; set; } = null!;
[ForeignKey(nameof(FogBugzCaseId))]
public FogBugzCase? FogBugzCase { get; set; }
// Helper methods for ModifiedFiles JSON
public List<string> GetModifiedFiles()
{
if (string.IsNullOrEmpty(ModifiedFiles))
return new List<string>();
try
{
return JsonSerializer.Deserialize<List<string>>(ModifiedFiles) ?? new List<string>();
}
catch
{
return new List<string>();
}
}
public void SetModifiedFiles(List<string> files)
{
ModifiedFiles = JsonSerializer.Serialize(files);
}
}
}

View File

@@ -0,0 +1,69 @@
using System.ComponentModel.DataAnnotations;
namespace Database.Models
{
public class FogBugzCase : BaseEntity
{
[Required]
public int CaseId { get; set; }
[Required]
[MaxLength(500)]
public string Title { get; set; } = string.Empty;
[MaxLength(200)]
public string? Project { get; set; }
[MaxLength(200)]
public string? Area { get; set; }
[Required]
[MaxLength(50)]
public string Status { get; set; } = "Unknown";
public DateTime LastUpdated { get; set; }
public bool IsOpen { get; set; } = true;
// Navigation properties
public ICollection<FogBugzEvent> Events { get; set; } = new List<FogBugzEvent>();
// Helper methods
public string GetLatestReleaseNote()
{
return Events
.Where(e => e.EventType == "ReleaseNoted" && !string.IsNullOrEmpty(e.ReleaseNote))
.OrderByDescending(e => e.EventDate)
.FirstOrDefault()?.ReleaseNote ?? string.Empty;
}
public int? GetZendeskTicketNumber()
{
return Events
.Where(e => e.ZendeskNumber > 0)
.OrderByDescending(e => e.EventDate)
.FirstOrDefault()?.ZendeskNumber;
}
public FogBugzStatus GetMappedStatus()
{
return Status.ToLower() switch
{
var s when s.StartsWith("open") => FogBugzStatus.Opened,
var s when s.StartsWith("resolved") => FogBugzStatus.Resolved,
var s when s.StartsWith("closed") => FogBugzStatus.Closed,
var s when s.StartsWith("reactivated") => FogBugzStatus.Reactivated,
_ => FogBugzStatus.Unknown
};
}
}
public enum FogBugzStatus
{
Unknown,
Opened,
Resolved,
Closed,
Reactivated
}
}

View File

@@ -0,0 +1,55 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Database.Models
{
public class FogBugzEvent : BaseEntity
{
public int CaseId { get; set; }
[Required]
[MaxLength(100)]
public string EventType { get; set; } = string.Empty;
[MaxLength(100)]
public string? User { get; set; }
public string? Comment { get; set; }
public string? StatusString { get; set; }
public DateTime EventDate { get; set; }
public string? ReleaseNote { get; set; }
public int? ZendeskNumber { get; set; }
// Navigation properties
[ForeignKey(nameof(CaseId))]
public FogBugzCase Case { get; set; } = null!;
// Helper properties
public bool IsReleaseNoteEvent => EventType == "ReleaseNoted" && !string.IsNullOrEmpty(ReleaseNote);
public bool IsZendeskEvent => EventType == "sZendesk" && ZendeskNumber.HasValue && ZendeskNumber > 0;
public bool IsStatusChangeEvent => !string.IsNullOrEmpty(StatusString) &&
(StatusString.StartsWith("Opened") || StatusString.StartsWith("Resolved") ||
StatusString.StartsWith("Closed") || StatusString.StartsWith("Reactivated"));
public FogBugzStatus GetMappedStatus()
{
if (string.IsNullOrEmpty(StatusString))
return FogBugzStatus.Unknown;
return StatusString.ToLower() switch
{
var s when s.StartsWith("opened") => FogBugzStatus.Opened,
var s when s.StartsWith("resolved") => FogBugzStatus.Resolved,
var s when s.StartsWith("closed") => FogBugzStatus.Closed,
var s when s.StartsWith("reactivated") => FogBugzStatus.Reactivated,
_ => FogBugzStatus.Unknown
};
}
}
}

View File

@@ -0,0 +1,93 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace Database.Models
{
public class HelpCenterProvider : BaseEntity
{
[Required]
[MaxLength(200)]
public string Name { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Type { get; set; } = string.Empty; // Zendesk, Salesforce
// JSON column for PostgreSQL
[Column(TypeName = "jsonb")]
public string? Configuration { get; set; }
public bool IsActive { get; set; } = true;
public string? Description { get; set; }
public DateTime? LastTested { get; set; }
public bool? LastTestResult { get; set; }
public string? LastTestError { get; set; }
// Helper methods for Configuration JSON
public HelpCenterProviderConfiguration GetConfiguration()
{
if (string.IsNullOrEmpty(Configuration))
return new HelpCenterProviderConfiguration();
try
{
return JsonSerializer.Deserialize<HelpCenterProviderConfiguration>(Configuration) ?? new HelpCenterProviderConfiguration();
}
catch
{
return new HelpCenterProviderConfiguration();
}
}
public void SetConfiguration(HelpCenterProviderConfiguration config)
{
Configuration = JsonSerializer.Serialize(config);
}
// Helper properties
public bool IsZendeskProvider => Type.Equals("Zendesk", StringComparison.OrdinalIgnoreCase);
public bool IsSalesforceProvider => Type.Equals("Salesforce", StringComparison.OrdinalIgnoreCase);
}
public class HelpCenterProviderConfiguration
{
// Common settings
public string? BaseUrl { get; set; }
public string? ApiVersion { get; set; }
public string? DefaultLocale { get; set; } = "en-us";
// Zendesk specific
public string? Subdomain { get; set; }
public string? Username { get; set; }
public string? ApiKey { get; set; }
public string? Token { get; set; }
// Salesforce specific
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public string? SecurityToken { get; set; }
public string? LoginUrl { get; set; }
// Template variables
public Dictionary<string, string> DefaultTemplateVariables { get; set; } = new()
{
["VERSION"] = "{{VERSION}}",
["SWURL"] = "{{SWURL}}",
["PDFURL"] = "{{PDFURL}}",
["DATE"] = "{{DATE}}",
["PROJECT"] = "{{PROJECT}}"
};
// Additional settings
public Dictionary<string, object> AdditionalSettings { get; set; } = new();
public Dictionary<string, string> Headers { get; set; } = new();
}
}

View File

@@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Database.Models
{
public class Package : BaseEntity
{
[Required]
[MaxLength(200)]
public string Title { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Version { get; set; } = string.Empty;
public string? Description { get; set; }
public int ProjectId { get; set; }
public int? SourceBuildId { get; set; }
[Required]
[MaxLength(50)]
public string Status { get; set; } = "Draft"; // Draft, Ready, Publishing, Published, Failed
public DateTime? PublishDate { get; set; }
// Navigation properties
[ForeignKey(nameof(ProjectId))]
public Project Project { get; set; } = null!;
[ForeignKey(nameof(SourceBuildId))]
public Build? SourceBuild { get; set; }
public PackageConfiguration? Configuration { get; set; }
public ICollection<Publication> Publications { get; set; } = new List<Publication>();
// Helper properties
public bool IsPublished => Status == "Published" && PublishDate.HasValue;
public bool IsReadyToPublish => Status == "Ready" && SourceBuildId.HasValue;
}
}

View File

@@ -0,0 +1,92 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace Database.Models
{
public class PackageConfiguration : BaseEntity
{
public int PackageId { get; set; }
public string? BuildFolder { get; set; }
public bool ZipContents { get; set; } = false;
public bool DeleteOldPublishedBuilds { get; set; } = false;
public string? ReleaseNoteTemplate { get; set; }
// JSON columns for PostgreSQL
[Column(TypeName = "jsonb")]
public string? StorageSettings { get; set; }
[Column(TypeName = "jsonb")]
public string? HelpCenterSettings { get; set; }
// Navigation properties
[ForeignKey(nameof(PackageId))]
public Package Package { get; set; } = null!;
// Helper methods for StorageSettings JSON
public StorageSettingsModel GetStorageSettings()
{
if (string.IsNullOrEmpty(StorageSettings))
return new StorageSettingsModel();
try
{
return JsonSerializer.Deserialize<StorageSettingsModel>(StorageSettings) ?? new StorageSettingsModel();
}
catch
{
return new StorageSettingsModel();
}
}
public void SetStorageSettings(StorageSettingsModel settings)
{
StorageSettings = JsonSerializer.Serialize(settings);
}
// Helper methods for HelpCenterSettings JSON
public HelpCenterSettingsModel GetHelpCenterSettings()
{
if (string.IsNullOrEmpty(HelpCenterSettings))
return new HelpCenterSettingsModel();
try
{
return JsonSerializer.Deserialize<HelpCenterSettingsModel>(HelpCenterSettings) ?? new HelpCenterSettingsModel();
}
catch
{
return new HelpCenterSettingsModel();
}
}
public void SetHelpCenterSettings(HelpCenterSettingsModel settings)
{
HelpCenterSettings = JsonSerializer.Serialize(settings);
}
}
public class StorageSettingsModel
{
public string? ProviderName { get; set; }
public string? ContainerName { get; set; }
public string? FolderPath { get; set; }
public bool EnableCDN { get; set; } = false;
public int? MaxVersionsToKeep { get; set; }
public Dictionary<string, object> AdditionalSettings { get; set; } = new();
}
public class HelpCenterSettingsModel
{
public string? ProviderName { get; set; }
public string? ArticleId { get; set; }
public string? ArticleTemplate { get; set; }
public string? Locale { get; set; } = "en-us";
public Dictionary<string, string> TemplateVariables { get; set; } = new();
public Dictionary<string, object> AdditionalSettings { get; set; } = new();
}
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
namespace Database.Models
{
public class Project : BaseEntity
{
[Required]
[MaxLength(200)]
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
[Required]
[MaxLength(200)]
public string CCNetProjectName { get; set; } = string.Empty;
[MaxLength(50)]
public string Status { get; set; } = "Active"; // Active, Inactive, Archived
// Navigation properties
public ICollection<Build> Builds { get; set; } = new List<Build>();
public ICollection<Package> Packages { get; set; } = new List<Package>();
}
}

View File

@@ -0,0 +1,75 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace Database.Models
{
public class Publication : BaseEntity
{
public int PackageId { get; set; }
[Required]
[MaxLength(50)]
public string Status { get; set; } = "Pending"; // Pending, InProgress, Completed, Failed, Cancelled
public DateTime? PublishedAt { get; set; }
public string? ReleaseNotesPath { get; set; }
// JSON column for PostgreSQL
[Column(TypeName = "jsonb")]
public string? PublicationDetails { get; set; }
public string? ErrorMessage { get; set; }
public TimeSpan? Duration => PublishedAt?.Subtract(CreatedAt);
// Navigation properties
[ForeignKey(nameof(PackageId))]
public Package Package { get; set; } = null!;
public ICollection<PublishingStep> Steps { get; set; } = new List<PublishingStep>();
// Helper methods for PublicationDetails JSON
public PublicationDetailsModel GetPublicationDetails()
{
if (string.IsNullOrEmpty(PublicationDetails))
return new PublicationDetailsModel();
try
{
return JsonSerializer.Deserialize<PublicationDetailsModel>(PublicationDetails) ?? new PublicationDetailsModel();
}
catch
{
return new PublicationDetailsModel();
}
}
public void SetPublicationDetails(PublicationDetailsModel details)
{
PublicationDetails = JsonSerializer.Serialize(details);
}
// Helper properties
public bool IsCompleted => Status == "Completed" && PublishedAt.HasValue;
public bool IsFailed => Status == "Failed";
public bool IsInProgress => Status == "InProgress";
public int ProgressPercentage => Steps.Any() ?
(int)(Steps.Count(s => s.Status == "Completed") * 100.0 / Steps.Count) : 0;
}
public class PublicationDetailsModel
{
public string? BuildVersion { get; set; }
public string? ReleaseNotesUrl { get; set; }
public string? PackageUrl { get; set; }
public string? CDNUrl { get; set; }
public List<string> UploadedFiles { get; set; } = new();
public List<string> UpdatedArticles { get; set; } = new();
public Dictionary<string, object> AdditionalDetails { get; set; } = new();
}
}

View File

@@ -0,0 +1,104 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Database.Models
{
public class PublishingStep : BaseEntity
{
public int PublicationId { get; set; }
[Required]
[MaxLength(200)]
public string StepName { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Status { get; set; } = "Pending"; // Pending, InProgress, Completed, Failed, Skipped
public string? Details { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
[Range(0, 100)]
public int ProgressPercent { get; set; } = 0;
public string? ErrorMessage { get; set; }
public int StepOrder { get; set; }
// Navigation properties
[ForeignKey(nameof(PublicationId))]
public Publication Publication { get; set; } = null!;
// Helper properties
public TimeSpan? Duration => CompletedAt?.Subtract(StartedAt ?? CreatedAt);
public bool IsCompleted => Status == "Completed" && CompletedAt.HasValue;
public bool IsFailed => Status == "Failed";
public bool IsInProgress => Status == "InProgress";
public bool IsPending => Status == "Pending";
public void Start()
{
Status = "InProgress";
StartedAt = DateTime.UtcNow;
UpdateTimestamp("System");
}
public void Complete(string? details = null)
{
Status = "Completed";
CompletedAt = DateTime.UtcNow;
ProgressPercent = 100;
if (details != null)
Details = details;
UpdateTimestamp("System");
}
public void Fail(string errorMessage, string? details = null)
{
Status = "Failed";
CompletedAt = DateTime.UtcNow;
ErrorMessage = errorMessage;
if (details != null)
Details = details;
UpdateTimestamp("System");
}
public void Skip(string reason)
{
Status = "Skipped";
CompletedAt = DateTime.UtcNow;
Details = reason;
ProgressPercent = 100;
UpdateTimestamp("System");
}
public void UpdateProgress(int percent, string? details = null)
{
ProgressPercent = Math.Max(0, Math.Min(100, percent));
if (details != null)
Details = details;
UpdateTimestamp("System");
}
}
public static class PublishingStepNames
{
public const string ValidateConfiguration = "Validate Configuration";
public const string CollectBuildData = "Collect Build Data";
public const string QueryFogBugz = "Query FogBugz Data";
public const string GenerateReleaseNotes = "Generate Release Notes";
public const string CompressFiles = "Compress Files";
public const string UploadToStorage = "Upload to Storage";
public const string CleanupOldVersions = "Cleanup Old Versions";
public const string UpdateHelpCenter = "Update Help Center";
public const string UpdatePackageStatus = "Update Package Status";
public const string SendNotification = "Send Notification";
}
}

View File

@@ -0,0 +1,90 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace Database.Models
{
public class StorageProvider : BaseEntity
{
[Required]
[MaxLength(200)]
public string Name { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Type { get; set; } = string.Empty; // S3, Box, FTP
// JSON column for PostgreSQL
[Column(TypeName = "jsonb")]
public string? Configuration { get; set; }
public bool IsActive { get; set; } = true;
public string? Description { get; set; }
public DateTime? LastTested { get; set; }
public bool? LastTestResult { get; set; }
public string? LastTestError { get; set; }
// Helper methods for Configuration JSON
public StorageProviderConfiguration GetConfiguration()
{
if (string.IsNullOrEmpty(Configuration))
return new StorageProviderConfiguration();
try
{
return JsonSerializer.Deserialize<StorageProviderConfiguration>(Configuration) ?? new StorageProviderConfiguration();
}
catch
{
return new StorageProviderConfiguration();
}
}
public void SetConfiguration(StorageProviderConfiguration config)
{
Configuration = JsonSerializer.Serialize(config);
}
// Helper properties
public bool IsS3Provider => Type.Equals("S3", StringComparison.OrdinalIgnoreCase);
public bool IsBoxProvider => Type.Equals("Box", StringComparison.OrdinalIgnoreCase);
public bool IsFtpProvider => Type.Equals("FTP", StringComparison.OrdinalIgnoreCase);
}
public class StorageProviderConfiguration
{
// Common settings
public string? ConnectionString { get; set; }
public string? Region { get; set; }
public string? Bucket { get; set; }
public string? Container { get; set; }
// AWS S3 specific
public string? AccessKey { get; set; }
public string? SecretKey { get; set; }
public string? CloudFrontDomain { get; set; }
// Box specific
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
// FTP specific
public string? Host { get; set; }
public int? Port { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public bool? UseSSL { get; set; }
// Additional settings
public Dictionary<string, object> AdditionalSettings { get; set; } = new();
public Dictionary<string, string> Headers { get; set; } = new();
}
}

View File

@@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
namespace Database.Models
{
public class User : BaseEntity
{
[Required]
[MaxLength(100)]
public string Username { get; set; } = string.Empty;
[Required]
public string PasswordHash { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Role { get; set; } = "User"; // Admin, User, ReadOnly
public DateTime? LastLogin { get; set; }
public string? Email { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public bool IsActive { get; set; } = true;
public string FullName => $"{FirstName} {LastName}".Trim();
}
}

View File

@@ -0,0 +1,129 @@
using Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Database.Services
{
public interface IBuildRepository
{
Task<Build?> GetByIdAsync(int id);
Task<Build?> GetByProjectAndBuildNumberAsync(int projectId, string buildNumber);
Task<List<Build>> GetByProjectIdAsync(int projectId);
Task<List<Build>> GetSuccessfulBuildsByProjectIdAsync(int projectId);
Task<List<Build>> GetRecentBuildsAsync(int count = 50);
Task<Build> CreateAsync(Build build);
Task<Build> UpdateAsync(Build build);
Task DeleteAsync(int id, string deletedBy);
Task<bool> ExistsAsync(int id);
Task<List<BuildCommit>> GetCommitsByBuildIdAsync(int buildId);
Task<BuildCommit> CreateCommitAsync(BuildCommit commit);
Task<List<BuildCommit>> CreateCommitsAsync(List<BuildCommit> commits);
}
public class BuildRepository : IBuildRepository
{
private readonly DatabaseContext _context;
public BuildRepository(DatabaseContext context)
{
_context = context;
}
public async Task<Build?> GetByIdAsync(int id)
{
return await _context.Builds
.Include(b => b.Project)
.Include(b => b.Commits)
.ThenInclude(c => c.FogBugzCase)
.FirstOrDefaultAsync(b => b.Id == id);
}
public async Task<Build?> GetByProjectAndBuildNumberAsync(int projectId, string buildNumber)
{
return await _context.Builds
.Include(b => b.Project)
.Include(b => b.Commits)
.FirstOrDefaultAsync(b => b.ProjectId == projectId && b.BuildNumber == buildNumber);
}
public async Task<List<Build>> GetByProjectIdAsync(int projectId)
{
return await _context.Builds
.Include(b => b.Project)
.Where(b => b.ProjectId == projectId)
.OrderByDescending(b => b.StartTime)
.ToListAsync();
}
public async Task<List<Build>> GetSuccessfulBuildsByProjectIdAsync(int projectId)
{
return await _context.Builds
.Include(b => b.Project)
.Where(b => b.ProjectId == projectId && b.Status == "Success")
.OrderByDescending(b => b.StartTime)
.ToListAsync();
}
public async Task<List<Build>> GetRecentBuildsAsync(int count = 50)
{
return await _context.Builds
.Include(b => b.Project)
.OrderByDescending(b => b.StartTime)
.Take(count)
.ToListAsync();
}
public async Task<Build> CreateAsync(Build build)
{
_context.Builds.Add(build);
await _context.SaveChangesAsync();
return build;
}
public async Task<Build> UpdateAsync(Build build)
{
build.UpdateTimestamp(build.UpdatedBy ?? "System");
_context.Builds.Update(build);
await _context.SaveChangesAsync();
return build;
}
public async Task DeleteAsync(int id, string deletedBy)
{
var build = await GetByIdAsync(id);
if (build != null)
{
build.MarkAsDeleted(deletedBy);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Builds.AnyAsync(b => b.Id == id);
}
public async Task<List<BuildCommit>> GetCommitsByBuildIdAsync(int buildId)
{
return await _context.BuildCommits
.Include(c => c.Build)
.Include(c => c.FogBugzCase)
.Where(c => c.BuildId == buildId)
.OrderByDescending(c => c.CommitDate)
.ToListAsync();
}
public async Task<BuildCommit> CreateCommitAsync(BuildCommit commit)
{
_context.BuildCommits.Add(commit);
await _context.SaveChangesAsync();
return commit;
}
public async Task<List<BuildCommit>> CreateCommitsAsync(List<BuildCommit> commits)
{
_context.BuildCommits.AddRange(commits);
await _context.SaveChangesAsync();
return commits;
}
}
}

View File

@@ -0,0 +1,158 @@
using Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Database.Services
{
public interface IFogBugzRepository
{
Task<FogBugzCase?> GetCaseByIdAsync(int caseId);
Task<FogBugzCase?> GetCaseByDbIdAsync(int id);
Task<List<FogBugzCase>> GetCasesByStatusAsync(string status);
Task<List<FogBugzCase>> GetRecentCasesAsync(int count = 50);
Task<FogBugzCase> CreateOrUpdateCaseAsync(FogBugzCase fogBugzCase);
Task DeleteCaseAsync(int id, string deletedBy);
Task<bool> CaseExistsAsync(int caseId);
Task<List<FogBugzEvent>> GetEventsByCaseIdAsync(int caseId);
Task<FogBugzEvent> CreateEventAsync(FogBugzEvent fogBugzEvent);
Task<List<FogBugzEvent>> CreateEventsAsync(List<FogBugzEvent> events);
Task<List<FogBugzEvent>> GetReleaseNoteEventsAsync(int caseId);
Task<List<FogBugzEvent>> GetZendeskEventsAsync(int caseId);
}
public class FogBugzRepository : IFogBugzRepository
{
private readonly DatabaseContext _context;
public FogBugzRepository(DatabaseContext context)
{
_context = context;
}
public async Task<FogBugzCase?> GetCaseByIdAsync(int caseId)
{
return await _context.FogBugzCases
.Include(c => c.Events.OrderByDescending(e => e.EventDate))
.FirstOrDefaultAsync(c => c.CaseId == caseId);
}
public async Task<FogBugzCase?> GetCaseByDbIdAsync(int id)
{
return await _context.FogBugzCases
.Include(c => c.Events.OrderByDescending(e => e.EventDate))
.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<List<FogBugzCase>> GetCasesByStatusAsync(string status)
{
return await _context.FogBugzCases
.Where(c => c.Status == status)
.OrderByDescending(c => c.LastUpdated)
.ToListAsync();
}
public async Task<List<FogBugzCase>> GetRecentCasesAsync(int count = 50)
{
return await _context.FogBugzCases
.OrderByDescending(c => c.LastUpdated)
.Take(count)
.ToListAsync();
}
public async Task<FogBugzCase> CreateOrUpdateCaseAsync(FogBugzCase fogBugzCase)
{
var existing = await GetCaseByIdAsync(fogBugzCase.CaseId);
if (existing != null)
{
existing.Title = fogBugzCase.Title;
existing.Project = fogBugzCase.Project;
existing.Area = fogBugzCase.Area;
existing.Status = fogBugzCase.Status;
existing.LastUpdated = fogBugzCase.LastUpdated;
existing.IsOpen = fogBugzCase.IsOpen;
existing.UpdateTimestamp(fogBugzCase.UpdatedBy ?? "System");
_context.FogBugzCases.Update(existing);
await _context.SaveChangesAsync();
return existing;
}
else
{
_context.FogBugzCases.Add(fogBugzCase);
await _context.SaveChangesAsync();
return fogBugzCase;
}
}
public async Task DeleteCaseAsync(int id, string deletedBy)
{
var fogBugzCase = await GetCaseByDbIdAsync(id);
if (fogBugzCase != null)
{
fogBugzCase.MarkAsDeleted(deletedBy);
await _context.SaveChangesAsync();
}
}
public async Task<bool> CaseExistsAsync(int caseId)
{
return await _context.FogBugzCases.AnyAsync(c => c.CaseId == caseId);
}
public async Task<List<FogBugzEvent>> GetEventsByCaseIdAsync(int caseId)
{
var fogBugzCase = await GetCaseByIdAsync(caseId);
if (fogBugzCase == null)
return new List<FogBugzEvent>();
return await _context.FogBugzEvents
.Where(e => e.CaseId == fogBugzCase.Id)
.OrderByDescending(e => e.EventDate)
.ToListAsync();
}
public async Task<FogBugzEvent> CreateEventAsync(FogBugzEvent fogBugzEvent)
{
_context.FogBugzEvents.Add(fogBugzEvent);
await _context.SaveChangesAsync();
return fogBugzEvent;
}
public async Task<List<FogBugzEvent>> CreateEventsAsync(List<FogBugzEvent> events)
{
_context.FogBugzEvents.AddRange(events);
await _context.SaveChangesAsync();
return events;
}
public async Task<List<FogBugzEvent>> GetReleaseNoteEventsAsync(int caseId)
{
var fogBugzCase = await GetCaseByIdAsync(caseId);
if (fogBugzCase == null)
return new List<FogBugzEvent>();
return await _context.FogBugzEvents
.Where(e => e.CaseId == fogBugzCase.Id &&
e.EventType == "ReleaseNoted" &&
!string.IsNullOrEmpty(e.ReleaseNote))
.OrderByDescending(e => e.EventDate)
.ToListAsync();
}
public async Task<List<FogBugzEvent>> GetZendeskEventsAsync(int caseId)
{
var fogBugzCase = await GetCaseByIdAsync(caseId);
if (fogBugzCase == null)
return new List<FogBugzEvent>();
return await _context.FogBugzEvents
.Where(e => e.CaseId == fogBugzCase.Id &&
e.EventType == "sZendesk" &&
e.ZendeskNumber.HasValue &&
e.ZendeskNumber > 0)
.OrderByDescending(e => e.EventDate)
.ToListAsync();
}
}
}

View File

@@ -0,0 +1,130 @@
using Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Database.Services
{
public interface IPackageRepository
{
Task<Package?> GetByIdAsync(int id);
Task<List<Package>> GetAllAsync();
Task<List<Package>> GetByProjectIdAsync(int projectId);
Task<List<Package>> GetByStatusAsync(string status);
Task<Package> CreateAsync(Package package);
Task<Package> UpdateAsync(Package package);
Task DeleteAsync(int id, string deletedBy);
Task<bool> ExistsAsync(int id);
Task<PackageConfiguration?> GetConfigurationAsync(int packageId);
Task<PackageConfiguration> SaveConfigurationAsync(PackageConfiguration config);
}
public class PackageRepository : IPackageRepository
{
private readonly DatabaseContext _context;
public PackageRepository(DatabaseContext context)
{
_context = context;
}
public async Task<Package?> GetByIdAsync(int id)
{
return await _context.Packages
.Include(p => p.Project)
.Include(p => p.SourceBuild)
.Include(p => p.Configuration)
.Include(p => p.Publications)
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<List<Package>> GetAllAsync()
{
return await _context.Packages
.Include(p => p.Project)
.Include(p => p.SourceBuild)
.OrderByDescending(p => p.CreatedAt)
.ToListAsync();
}
public async Task<List<Package>> GetByProjectIdAsync(int projectId)
{
return await _context.Packages
.Include(p => p.Project)
.Include(p => p.SourceBuild)
.Where(p => p.ProjectId == projectId)
.OrderByDescending(p => p.CreatedAt)
.ToListAsync();
}
public async Task<List<Package>> GetByStatusAsync(string status)
{
return await _context.Packages
.Include(p => p.Project)
.Include(p => p.SourceBuild)
.Where(p => p.Status == status)
.OrderByDescending(p => p.CreatedAt)
.ToListAsync();
}
public async Task<Package> CreateAsync(Package package)
{
_context.Packages.Add(package);
await _context.SaveChangesAsync();
return package;
}
public async Task<Package> UpdateAsync(Package package)
{
package.UpdateTimestamp(package.UpdatedBy ?? "System");
_context.Packages.Update(package);
await _context.SaveChangesAsync();
return package;
}
public async Task DeleteAsync(int id, string deletedBy)
{
var package = await GetByIdAsync(id);
if (package != null)
{
package.MarkAsDeleted(deletedBy);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Packages.AnyAsync(p => p.Id == id);
}
public async Task<PackageConfiguration?> GetConfigurationAsync(int packageId)
{
return await _context.PackageConfigurations
.FirstOrDefaultAsync(pc => pc.PackageId == packageId);
}
public async Task<PackageConfiguration> SaveConfigurationAsync(PackageConfiguration config)
{
var existing = await GetConfigurationAsync(config.PackageId);
if (existing != null)
{
existing.BuildFolder = config.BuildFolder;
existing.ZipContents = config.ZipContents;
existing.DeleteOldPublishedBuilds = config.DeleteOldPublishedBuilds;
existing.ReleaseNoteTemplate = config.ReleaseNoteTemplate;
existing.StorageSettings = config.StorageSettings;
existing.HelpCenterSettings = config.HelpCenterSettings;
existing.UpdateTimestamp(config.UpdatedBy ?? "System");
_context.PackageConfigurations.Update(existing);
await _context.SaveChangesAsync();
return existing;
}
else
{
_context.PackageConfigurations.Add(config);
await _context.SaveChangesAsync();
return config;
}
}
}
}

View File

@@ -0,0 +1,96 @@
using Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Database.Services
{
public interface IProjectRepository
{
Task<Project?> GetByIdAsync(int id);
Task<Project?> GetByCCNetNameAsync(string ccNetProjectName);
Task<List<Project>> GetAllAsync();
Task<List<Project>> GetActiveProjectsAsync();
Task<Project> CreateAsync(Project project);
Task<Project> UpdateAsync(Project project);
Task DeleteAsync(int id, string deletedBy);
Task<bool> ExistsAsync(int id);
Task<bool> CCNetNameExistsAsync(string ccNetProjectName);
}
public class ProjectRepository : IProjectRepository
{
private readonly DatabaseContext _context;
public ProjectRepository(DatabaseContext context)
{
_context = context;
}
public async Task<Project?> GetByIdAsync(int id)
{
return await _context.Projects
.Include(p => p.Builds)
.Include(p => p.Packages)
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<Project?> GetByCCNetNameAsync(string ccNetProjectName)
{
return await _context.Projects
.Include(p => p.Builds)
.Include(p => p.Packages)
.FirstOrDefaultAsync(p => p.CCNetProjectName == ccNetProjectName);
}
public async Task<List<Project>> GetAllAsync()
{
return await _context.Projects
.Include(p => p.Builds.Take(5))
.Include(p => p.Packages.Take(5))
.OrderBy(p => p.Name)
.ToListAsync();
}
public async Task<List<Project>> GetActiveProjectsAsync()
{
return await _context.Projects
.Where(p => p.Status == "Active")
.OrderBy(p => p.Name)
.ToListAsync();
}
public async Task<Project> CreateAsync(Project project)
{
_context.Projects.Add(project);
await _context.SaveChangesAsync();
return project;
}
public async Task<Project> UpdateAsync(Project project)
{
project.UpdateTimestamp(project.UpdatedBy ?? "System");
_context.Projects.Update(project);
await _context.SaveChangesAsync();
return project;
}
public async Task DeleteAsync(int id, string deletedBy)
{
var project = await GetByIdAsync(id);
if (project != null)
{
project.MarkAsDeleted(deletedBy);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Projects.AnyAsync(p => p.Id == id);
}
public async Task<bool> CCNetNameExistsAsync(string ccNetProjectName)
{
return await _context.Projects.AnyAsync(p => p.CCNetProjectName == ccNetProjectName);
}
}
}

View File

@@ -0,0 +1,168 @@
using Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Database.Services
{
public interface IPublicationRepository
{
Task<Publication?> GetByIdAsync(int id);
Task<List<Publication>> GetByPackageIdAsync(int packageId);
Task<List<Publication>> GetByStatusAsync(string status);
Task<List<Publication>> GetRecentPublicationsAsync(int count = 50);
Task<Publication> CreateAsync(Publication publication);
Task<Publication> UpdateAsync(Publication publication);
Task DeleteAsync(int id, string deletedBy);
Task<bool> ExistsAsync(int id);
Task<List<PublishingStep>> GetStepsByPublicationIdAsync(int publicationId);
Task<PublishingStep?> GetStepByIdAsync(int stepId);
Task<PublishingStep> CreateStepAsync(PublishingStep step);
Task<PublishingStep> UpdateStepAsync(PublishingStep step);
Task<List<PublishingStep>> CreateStepsAsync(List<PublishingStep> steps);
Task<PublishingStep?> GetCurrentStepAsync(int publicationId);
Task<List<PublishingStep>> GetCompletedStepsAsync(int publicationId);
Task<List<PublishingStep>> GetFailedStepsAsync(int publicationId);
}
public class PublicationRepository : IPublicationRepository
{
private readonly DatabaseContext _context;
public PublicationRepository(DatabaseContext context)
{
_context = context;
}
public async Task<Publication?> GetByIdAsync(int id)
{
return await _context.Publications
.Include(p => p.Package)
.ThenInclude(pkg => pkg.Project)
.Include(p => p.Steps.OrderBy(s => s.StepOrder))
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<List<Publication>> GetByPackageIdAsync(int packageId)
{
return await _context.Publications
.Include(p => p.Package)
.Include(p => p.Steps)
.Where(p => p.PackageId == packageId)
.OrderByDescending(p => p.CreatedAt)
.ToListAsync();
}
public async Task<List<Publication>> GetByStatusAsync(string status)
{
return await _context.Publications
.Include(p => p.Package)
.ThenInclude(pkg => pkg.Project)
.Where(p => p.Status == status)
.OrderByDescending(p => p.CreatedAt)
.ToListAsync();
}
public async Task<List<Publication>> GetRecentPublicationsAsync(int count = 50)
{
return await _context.Publications
.Include(p => p.Package)
.ThenInclude(pkg => pkg.Project)
.OrderByDescending(p => p.CreatedAt)
.Take(count)
.ToListAsync();
}
public async Task<Publication> CreateAsync(Publication publication)
{
_context.Publications.Add(publication);
await _context.SaveChangesAsync();
return publication;
}
public async Task<Publication> UpdateAsync(Publication publication)
{
publication.UpdateTimestamp(publication.UpdatedBy ?? "System");
_context.Publications.Update(publication);
await _context.SaveChangesAsync();
return publication;
}
public async Task DeleteAsync(int id, string deletedBy)
{
var publication = await GetByIdAsync(id);
if (publication != null)
{
publication.MarkAsDeleted(deletedBy);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Publications.AnyAsync(p => p.Id == id);
}
public async Task<List<PublishingStep>> GetStepsByPublicationIdAsync(int publicationId)
{
return await _context.PublishingSteps
.Include(s => s.Publication)
.Where(s => s.PublicationId == publicationId)
.OrderBy(s => s.StepOrder)
.ToListAsync();
}
public async Task<PublishingStep?> GetStepByIdAsync(int stepId)
{
return await _context.PublishingSteps
.Include(s => s.Publication)
.FirstOrDefaultAsync(s => s.Id == stepId);
}
public async Task<PublishingStep> CreateStepAsync(PublishingStep step)
{
_context.PublishingSteps.Add(step);
await _context.SaveChangesAsync();
return step;
}
public async Task<PublishingStep> UpdateStepAsync(PublishingStep step)
{
step.UpdateTimestamp(step.UpdatedBy ?? "System");
_context.PublishingSteps.Update(step);
await _context.SaveChangesAsync();
return step;
}
public async Task<List<PublishingStep>> CreateStepsAsync(List<PublishingStep> steps)
{
_context.PublishingSteps.AddRange(steps);
await _context.SaveChangesAsync();
return steps;
}
public async Task<PublishingStep?> GetCurrentStepAsync(int publicationId)
{
return await _context.PublishingSteps
.Where(s => s.PublicationId == publicationId &&
(s.Status == "InProgress" || s.Status == "Pending"))
.OrderBy(s => s.StepOrder)
.FirstOrDefaultAsync();
}
public async Task<List<PublishingStep>> GetCompletedStepsAsync(int publicationId)
{
return await _context.PublishingSteps
.Where(s => s.PublicationId == publicationId && s.Status == "Completed")
.OrderBy(s => s.StepOrder)
.ToListAsync();
}
public async Task<List<PublishingStep>> GetFailedStepsAsync(int publicationId)
{
return await _context.PublishingSteps
.Where(s => s.PublicationId == publicationId && s.Status == "Failed")
.OrderBy(s => s.StepOrder)
.ToListAsync();
}
}
}

View File

@@ -0,0 +1,113 @@
using Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Database.Services
{
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id);
Task<User?> GetByUsernameAsync(string username);
Task<User?> GetByEmailAsync(string email);
Task<List<User>> GetAllAsync();
Task<List<User>> GetActiveUsersAsync();
Task<User> CreateAsync(User user);
Task<User> UpdateAsync(User user);
Task DeleteAsync(int id, string deletedBy);
Task<bool> ExistsAsync(int id);
Task<bool> UsernameExistsAsync(string username);
Task<bool> EmailExistsAsync(string email);
Task UpdateLastLoginAsync(int userId);
}
public class UserRepository : IUserRepository
{
private readonly DatabaseContext _context;
public UserRepository(DatabaseContext context)
{
_context = context;
}
public async Task<User?> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
public async Task<User?> GetByUsernameAsync(string username)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Username == username);
}
public async Task<User?> GetByEmailAsync(string email)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Email == email);
}
public async Task<List<User>> GetAllAsync()
{
return await _context.Users
.OrderBy(u => u.Username)
.ToListAsync();
}
public async Task<List<User>> GetActiveUsersAsync()
{
return await _context.Users
.Where(u => u.IsActive)
.OrderBy(u => u.Username)
.ToListAsync();
}
public async Task<User> CreateAsync(User user)
{
_context.Users.Add(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> UpdateAsync(User user)
{
user.UpdateTimestamp(user.UpdatedBy ?? "System");
_context.Users.Update(user);
await _context.SaveChangesAsync();
return user;
}
public async Task DeleteAsync(int id, string deletedBy)
{
var user = await GetByIdAsync(id);
if (user != null)
{
user.MarkAsDeleted(deletedBy);
await _context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
{
return await _context.Users.AnyAsync(u => u.Id == id);
}
public async Task<bool> UsernameExistsAsync(string username)
{
return await _context.Users.AnyAsync(u => u.Username == username);
}
public async Task<bool> EmailExistsAsync(string email)
{
return await _context.Users.AnyAsync(u => u.Email == email);
}
public async Task UpdateLastLoginAsync(int userId)
{
var user = await GetByIdAsync(userId);
if (user != null)
{
user.LastLogin = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
}
}

View File

@@ -0,0 +1,98 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Database.Configuration;
namespace Database.Services
{
public interface IDatabaseMigrator
{
Task MigrateAsync();
Task<bool> CanConnectAsync();
Task<List<string>> GetPendingMigrationsAsync();
Task<List<string>> GetAppliedMigrationsAsync();
}
public class DatabaseMigrator : IDatabaseMigrator
{
private readonly DatabaseContext _context;
private readonly DatabaseConfiguration _config;
private readonly ILogger<DatabaseMigrator> _logger;
public DatabaseMigrator(DatabaseContext context, DatabaseConfiguration config, ILogger<DatabaseMigrator> logger)
{
_context = context;
_config = config;
_logger = logger;
}
public async Task MigrateAsync()
{
try
{
_logger.LogInformation("Starting database migration...");
var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
{
_logger.LogInformation("Found {Count} pending migrations: {Migrations}",
pendingMigrations.Count(), string.Join(", ", pendingMigrations));
await _context.Database.MigrateAsync();
_logger.LogInformation("Database migration completed successfully");
}
else
{
_logger.LogInformation("No pending migrations found");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Database migration failed");
throw;
}
}
public async Task<bool> CanConnectAsync()
{
try
{
return await _context.Database.CanConnectAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to connect to database");
return false;
}
}
public async Task<List<string>> GetPendingMigrationsAsync()
{
try
{
var pending = await _context.Database.GetPendingMigrationsAsync();
return pending.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting pending migrations");
return new List<string>();
}
}
public async Task<List<string>> GetAppliedMigrationsAsync()
{
try
{
var applied = await _context.Database.GetAppliedMigrationsAsync();
return applied.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting applied migrations");
return new List<string>();
}
}
}
}

View File

@@ -0,0 +1,312 @@
using Microsoft.Extensions.Logging;
using Database.Models;
namespace Database.Services
{
public interface IDatabaseSeeder
{
Task SeedAsync();
Task SeedUsersAsync();
Task SeedProvidersAsync();
Task SeedTestDataAsync();
}
public class DatabaseSeeder : IDatabaseSeeder
{
private readonly DatabaseContext _context;
private readonly IPasswordHasher _passwordHasher;
private readonly ILogger<DatabaseSeeder> _logger;
public DatabaseSeeder(DatabaseContext context, IPasswordHasher passwordHasher, ILogger<DatabaseSeeder> logger)
{
_context = context;
_passwordHasher = passwordHasher;
_logger = logger;
}
public async Task SeedAsync()
{
try
{
_logger.LogInformation("Starting database seeding...");
await SeedUsersAsync();
await SeedProvidersAsync();
await _context.SaveChangesAsync();
_logger.LogInformation("Database seeding completed successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Database seeding failed");
throw;
}
}
public async Task SeedUsersAsync()
{
if (!_context.Users.Any())
{
_logger.LogInformation("Seeding default users...");
var adminUser = new User
{
Username = "admin",
PasswordHash = _passwordHasher.HashPassword("admin123"),
Role = "Admin",
Email = "admin@company.com",
FirstName = "System",
LastName = "Administrator",
IsActive = true,
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
};
var testUser = new User
{
Username = "user",
PasswordHash = _passwordHasher.HashPassword("user123"),
Role = "User",
Email = "user@company.com",
FirstName = "Test",
LastName = "User",
IsActive = true,
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
};
var readOnlyUser = new User
{
Username = "readonly",
PasswordHash = _passwordHasher.HashPassword("readonly123"),
Role = "ReadOnly",
Email = "readonly@company.com",
FirstName = "ReadOnly",
LastName = "User",
IsActive = true,
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
};
_context.Users.AddRange(adminUser, testUser, readOnlyUser);
_logger.LogInformation("Added {Count} default users", 3);
}
}
public async Task SeedProvidersAsync()
{
await SeedStorageProvidersAsync();
await SeedHelpCenterProvidersAsync();
}
private async Task SeedStorageProvidersAsync()
{
if (!_context.StorageProviders.Any())
{
_logger.LogInformation("Seeding default storage providers...");
var s3Provider = new StorageProvider
{
Name = "AWS S3",
Type = "S3",
Description = "Amazon Web Services S3 Storage",
IsActive = true,
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
};
s3Provider.SetConfiguration(new StorageProviderConfiguration
{
Region = "us-east-2",
Bucket = "software-releases",
CloudFrontDomain = "https://cdn.company.com/",
AdditionalSettings = new Dictionary<string, object>
{
["EnableCDN"] = true,
["EnableVersioning"] = true,
["DefaultExpiration"] = 365
}
});
var boxProvider = new StorageProvider
{
Name = "Box.com",
Type = "Box",
Description = "Box.com Cloud Storage",
IsActive = false,
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
};
boxProvider.SetConfiguration(new StorageProviderConfiguration
{
Container = "software-releases",
AdditionalSettings = new Dictionary<string, object>
{
["EnableSharedLinks"] = true,
["DefaultPermission"] = "view"
}
});
var ftpProvider = new StorageProvider
{
Name = "FTP Server",
Type = "FTP",
Description = "Legacy FTP Server",
IsActive = false,
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
};
ftpProvider.SetConfiguration(new StorageProviderConfiguration
{
Host = "ftp.company.com",
Port = 21,
UseSSL = false,
AdditionalSettings = new Dictionary<string, object>
{
["PassiveMode"] = true,
["BinaryMode"] = true
}
});
_context.StorageProviders.AddRange(s3Provider, boxProvider, ftpProvider);
_logger.LogInformation("Added {Count} storage providers", 3);
}
}
private async Task SeedHelpCenterProvidersAsync()
{
if (!_context.HelpCenterProviders.Any())
{
_logger.LogInformation("Seeding default help center providers...");
var zendeskProvider = new HelpCenterProvider
{
Name = "Zendesk",
Type = "Zendesk",
Description = "Zendesk Help Center Integration",
IsActive = true,
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
};
zendeskProvider.SetConfiguration(new HelpCenterProviderConfiguration
{
Subdomain = "company",
ApiVersion = "v2",
DefaultLocale = "en-us",
DefaultTemplateVariables = new Dictionary<string, string>
{
["VERSION"] = "{{VERSION}}",
["SWURL"] = "{{SWURL}}",
["PDFURL"] = "{{PDFURL}}",
["DATE"] = "{{DATE}}",
["PROJECT"] = "{{PROJECT}}"
}
});
var salesforceProvider = new HelpCenterProvider
{
Name = "Salesforce Service",
Type = "Salesforce",
Description = "Salesforce Service Cloud Integration",
IsActive = false,
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
};
salesforceProvider.SetConfiguration(new HelpCenterProviderConfiguration
{
LoginUrl = "https://login.salesforce.com",
ApiVersion = "v58.0",
DefaultLocale = "en-us"
});
_context.HelpCenterProviders.AddRange(zendeskProvider, salesforceProvider);
_logger.LogInformation("Added {Count} help center providers", 2);
}
}
public async Task SeedTestDataAsync()
{
_logger.LogInformation("Seeding test data...");
await SeedTestProjectsAsync();
await SeedTestPackagesAsync();
await _context.SaveChangesAsync();
_logger.LogInformation("Test data seeding completed");
}
private async Task SeedTestProjectsAsync()
{
if (!_context.Projects.Any())
{
var projects = new[]
{
new Project
{
Name = "DataPRO LTS",
Description = "Long Term Support version of DataPRO",
CCNetProjectName = "DATAPRO_MAINT_4_00",
Status = "Active",
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
},
new Project
{
Name = "DataPRO Stable",
Description = "Stable release version of DataPRO",
CCNetProjectName = "DATAPRO_MAINT_4_04",
Status = "Active",
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
}
};
_context.Projects.AddRange(projects);
}
}
private async Task SeedTestPackagesAsync()
{
if (!_context.Packages.Any() && _context.Projects.Any())
{
var ltsProject = _context.Projects.First(p => p.CCNetProjectName == "DATAPRO_MAINT_4_00");
var stableProject = _context.Projects.First(p => p.CCNetProjectName == "DATAPRO_MAINT_4_04");
var packages = new[]
{
new Package
{
Title = "DataPRO LTS Release",
Version = "4.0.1205",
Description = "Long term support release for enterprise customers",
ProjectId = ltsProject.Id,
Status = "Draft",
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
},
new Package
{
Title = "DataPRO Stable Release",
Version = "4.4.305",
Description = "Latest stable release with new features",
ProjectId = stableProject.Id,
Status = "Draft",
CreatedBy = "System",
CreatedAt = DateTime.UtcNow
}
};
_context.Packages.AddRange(packages);
}
}
}
}

View File

@@ -0,0 +1,38 @@
using BCrypt.Net;
namespace Database.Services
{
public interface IPasswordHasher
{
string HashPassword(string password);
bool VerifyPassword(string password, string hashedPassword);
}
public class PasswordHasher : IPasswordHasher
{
private const int WorkFactor = 12;
public string HashPassword(string password)
{
if (string.IsNullOrEmpty(password))
throw new ArgumentException("Password cannot be null or empty", nameof(password));
return BCrypt.Net.BCrypt.HashPassword(password, WorkFactor);
}
public bool VerifyPassword(string password, string hashedPassword)
{
if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(hashedPassword))
return false;
try
{
return BCrypt.Net.BCrypt.Verify(password, hashedPassword);
}
catch
{
return false;
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"Database": {
"EnableSensitiveDataLogging": true,
"EnableDetailedErrors": true,
"SeedTestData": true,
"AutoMigrate": true
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Information",
"Microsoft.EntityFrameworkCore": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
}
}
}

View File

@@ -0,0 +1,28 @@
{
"Database": {
"EnableSensitiveDataLogging": false,
"EnableDetailedErrors": false,
"CommandTimeout": 60,
"SeedTestData": false,
"AutoMigrate": false,
"Environment": "Production",
"Pooling": {
"MinPoolSize": 10,
"MaxPoolSize": 200,
"ConnectionIdleLifetime": 600,
"ConnectionPruningInterval": 30
},
"Retry": {
"EnableRetryOnFailure": true,
"MaxRetryCount": 3,
"MaxRetryDelay": 60
}
},
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Error"
}
}
}

View File

@@ -0,0 +1,31 @@
{
"Database": {
"ConnectionString": "Host=localhost;Port=5432;Database=software_release_management;Username=postgres;Password=postgres",
"EnableSensitiveDataLogging": false,
"EnableDetailedErrors": false,
"CommandTimeout": 30,
"AutoMigrate": false,
"SeedTestData": false,
"Environment": "Development",
"Pooling": {
"MinPoolSize": 5,
"MaxPoolSize": 100,
"ConnectionIdleLifetime": 300,
"ConnectionPruningInterval": 10
},
"Retry": {
"EnableRetryOnFailure": true,
"MaxRetryCount": 5,
"MaxRetryDelay": 30,
"ErrorNumbersToAdd": []
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Warning"
}
}
}