generated from noisedestroyers/claude
Merge branch 'noisedestroyers/database'
This commit is contained in:
441
README-Database.md
Normal file
441
README-Database.md
Normal 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
73
docker-compose.yml
Normal 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
30
scripts/init-db.sql
Normal 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
23
scripts/servers.json
Normal 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
59
secrets.yaml
Normal 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"
|
||||
47
src/Database/Configuration/DatabaseConfiguration.cs
Normal file
47
src/Database/Configuration/DatabaseConfiguration.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
27
src/Database/Database.csproj
Normal file
27
src/Database/Database.csproj
Normal 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>
|
||||
360
src/Database/DatabaseContext.cs
Normal file
360
src/Database/DatabaseContext.cs
Normal 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')");
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Database/Extensions/ServiceCollectionExtensions.cs
Normal file
68
src/Database/Extensions/ServiceCollectionExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Database/Models/BaseEntity.cs
Normal file
37
src/Database/Models/BaseEntity.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/Database/Models/Build.cs
Normal file
34
src/Database/Models/Build.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
60
src/Database/Models/BuildCommit.cs
Normal file
60
src/Database/Models/BuildCommit.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/Database/Models/FogBugzCase.cs
Normal file
69
src/Database/Models/FogBugzCase.cs
Normal 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
|
||||
}
|
||||
}
|
||||
55
src/Database/Models/FogBugzEvent.cs
Normal file
55
src/Database/Models/FogBugzEvent.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/Database/Models/HelpCenterProvider.cs
Normal file
93
src/Database/Models/HelpCenterProvider.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
44
src/Database/Models/Package.cs
Normal file
44
src/Database/Models/Package.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
92
src/Database/Models/PackageConfiguration.cs
Normal file
92
src/Database/Models/PackageConfiguration.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
24
src/Database/Models/Project.cs
Normal file
24
src/Database/Models/Project.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
75
src/Database/Models/Publication.cs
Normal file
75
src/Database/Models/Publication.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
104
src/Database/Models/PublishingStep.cs
Normal file
104
src/Database/Models/PublishingStep.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
90
src/Database/Models/StorageProvider.cs
Normal file
90
src/Database/Models/StorageProvider.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
30
src/Database/Models/User.cs
Normal file
30
src/Database/Models/User.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
129
src/Database/Repositories/IBuildRepository.cs
Normal file
129
src/Database/Repositories/IBuildRepository.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/Database/Repositories/IFogBugzRepository.cs
Normal file
158
src/Database/Repositories/IFogBugzRepository.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/Database/Repositories/IPackageRepository.cs
Normal file
130
src/Database/Repositories/IPackageRepository.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/Database/Repositories/IProjectRepository.cs
Normal file
96
src/Database/Repositories/IProjectRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
168
src/Database/Repositories/IPublicationRepository.cs
Normal file
168
src/Database/Repositories/IPublicationRepository.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/Database/Repositories/IUserRepository.cs
Normal file
113
src/Database/Repositories/IUserRepository.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Database/Services/DatabaseMigrator.cs
Normal file
98
src/Database/Services/DatabaseMigrator.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
312
src/Database/Services/DatabaseSeeder.cs
Normal file
312
src/Database/Services/DatabaseSeeder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/Database/Services/PasswordHasher.cs
Normal file
38
src/Database/Services/PasswordHasher.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Database/appsettings.Development.json
Normal file
16
src/Database/appsettings.Development.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Database/appsettings.Production.json
Normal file
28
src/Database/appsettings.Production.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/Database/appsettings.json
Normal file
31
src/Database/appsettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user