Files
OpenFlare/internal/repository/openflare_pages_source.go
T
ryan 943818f7d4 refactor(repository): 收敛 model/repository 分层为唯一持久化入口
将 OpenFlare 与平台业务的数据访问从 model 与 apps 直连迁入 repository,
model 仅保留实体与无 IO 规则;补充 code-check 架构守卫与开发规范。
2026-07-24 17:00:17 +08:00

418 lines
13 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package repository
import (
"context"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
)
const pagesRowLockStrength = "UPDATE"
// WithPagesTx runs fn inside a database transaction for Pages multi-step work.
func WithPagesTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return db.DB(ctx).Transaction(fn)
}
// GetPagesProjectSourceByID loads a project source by primary key.
func GetPagesProjectSourceByID(ctx context.Context, id uint) (*model.PagesProjectSource, error) {
var source model.PagesProjectSource
if err := db.DB(ctx).Where("id = ?", id).First(&source).Error; err != nil {
return nil, err
}
return &source, nil
}
// GetPagesProjectSourceByProjectID loads the unique source for a project.
func GetPagesProjectSourceByProjectID(ctx context.Context, projectID uint) (*model.PagesProjectSource, error) {
var source model.PagesProjectSource
if err := db.DB(ctx).Where("project_id = ?", projectID).First(&source).Error; err != nil {
return nil, err
}
return &source, nil
}
// GetPagesProjectSourceByIDAndConfigVersion loads a source matching both id and config version.
func GetPagesProjectSourceByIDAndConfigVersion(
ctx context.Context,
id uint,
configVersion int,
) (*model.PagesProjectSource, error) {
var source model.PagesProjectSource
if err := db.DB(ctx).Where("id = ? AND config_version = ?", id, configVersion).First(&source).Error; err != nil {
return nil, err
}
return &source, nil
}
// GetPagesProjectSourceRuntimeBySourceID loads runtime for a source.
func GetPagesProjectSourceRuntimeBySourceID(
ctx context.Context,
sourceID uint,
) (*model.PagesProjectSourceRuntime, error) {
var runtime model.PagesProjectSourceRuntime
if err := db.DB(ctx).Where("source_id = ?", sourceID).First(&runtime).Error; err != nil {
return nil, err
}
return &runtime, nil
}
// GetPagesProjectSourceAndRuntimeByProjectID loads source and its runtime for a project.
func GetPagesProjectSourceAndRuntimeByProjectID(
ctx context.Context,
projectID uint,
) (*model.PagesProjectSource, *model.PagesProjectSourceRuntime, error) {
source, err := GetPagesProjectSourceByProjectID(ctx, projectID)
if err != nil {
return nil, nil, err
}
runtime, err := GetPagesProjectSourceRuntimeBySourceID(ctx, source.ID)
if err != nil {
return nil, nil, err
}
return source, runtime, nil
}
// CreatePagesProjectSourceTx creates a source row inside an existing transaction.
func CreatePagesProjectSourceTx(tx *gorm.DB, source *model.PagesProjectSource) error {
return tx.Create(source).Error
}
// CreatePagesProjectSourceRuntimeTx creates a runtime row inside an existing transaction.
func CreatePagesProjectSourceRuntimeTx(tx *gorm.DB, runtime *model.PagesProjectSourceRuntime) error {
return tx.Create(runtime).Error
}
// UpdatePagesProjectSourceTx applies partial updates to a source inside a transaction.
func UpdatePagesProjectSourceTx(tx *gorm.DB, source *model.PagesProjectSource, updates map[string]any) error {
if len(updates) == 0 {
return nil
}
return tx.Model(source).Updates(updates).Error
}
// UpdatePagesProjectSourceRuntimeTx applies partial updates to a runtime inside a transaction.
func UpdatePagesProjectSourceRuntimeTx(
tx *gorm.DB,
runtime *model.PagesProjectSourceRuntime,
updates map[string]any,
) error {
if len(updates) == 0 {
return nil
}
return tx.Model(runtime).Updates(updates).Error
}
// UpdatePagesProjectSourceRuntimeFieldTx updates a single column on a runtime row.
func UpdatePagesProjectSourceRuntimeFieldTx(
tx *gorm.DB,
runtime *model.PagesProjectSourceRuntime,
column string,
value any,
) error {
return tx.Model(runtime).Update(column, value).Error
}
// DeletePagesProjectSourceRuntimeBySourceIDTx deletes runtime rows for a source.
func DeletePagesProjectSourceRuntimeBySourceIDTx(tx *gorm.DB, sourceID uint) error {
return tx.Where("source_id = ?", sourceID).Delete(&model.PagesProjectSourceRuntime{}).Error
}
// DeletePagesProjectSourceTx deletes a source row inside a transaction.
func DeletePagesProjectSourceTx(tx *gorm.DB, source *model.PagesProjectSource) error {
return tx.Delete(source).Error
}
// LockPagesProjectByIDTx locks a project row for update.
func LockPagesProjectByIDTx(tx *gorm.DB, id uint) (*model.PagesProject, error) {
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&project, id).Error; err != nil {
return nil, err
}
return &project, nil
}
// LockPagesProjectSourceByProjectIDTx locks the source for a project.
func LockPagesProjectSourceByProjectIDTx(tx *gorm.DB, projectID uint) (*model.PagesProjectSource, error) {
var source model.PagesProjectSource
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("project_id = ?", projectID).
First(&source).Error; err != nil {
return nil, err
}
return &source, nil
}
// LockPagesProjectSourceByIDTx locks a source by id.
func LockPagesProjectSourceByIDTx(tx *gorm.DB, sourceID uint) (*model.PagesProjectSource, error) {
var source model.PagesProjectSource
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("id = ?", sourceID).
First(&source).Error; err != nil {
return nil, err
}
return &source, nil
}
// LockPagesProjectSourceByIDAndProjectIDTx locks a source matching both identifiers.
func LockPagesProjectSourceByIDAndProjectIDTx(
tx *gorm.DB,
sourceID uint,
projectID uint,
) (*model.PagesProjectSource, error) {
var source model.PagesProjectSource
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("id = ? AND project_id = ?", sourceID, projectID).
First(&source).Error; err != nil {
return nil, err
}
return &source, nil
}
// LockPagesProjectSourceRuntimeBySourceIDTx locks runtime for a source.
func LockPagesProjectSourceRuntimeBySourceIDTx(
tx *gorm.DB,
sourceID uint,
) (*model.PagesProjectSourceRuntime, error) {
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", sourceID).
First(&runtime).Error; err != nil {
return nil, err
}
return &runtime, nil
}
// GetPagesProjectSourceByIDTx loads a source by id without locking.
func GetPagesProjectSourceByIDTx(tx *gorm.DB, sourceID uint) (*model.PagesProjectSource, error) {
var source model.PagesProjectSource
if err := tx.Where("id = ?", sourceID).First(&source).Error; err != nil {
return nil, err
}
return &source, nil
}
// TryAcquirePagesSourceRuntimeLease conditionally claims an idle/expired lease when config matches.
func TryAcquirePagesSourceRuntimeLease(
ctx context.Context,
sourceID uint,
expectedConfigVersion int,
now time.Time,
updates map[string]any,
) (int64, error) {
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ?", sourceID).
Where("lease_expires_at IS NULL OR lease_expires_at <= ?", now).
Where(
"EXISTS (SELECT 1 FROM of_pages_project_sources source WHERE source.id = ? AND source.config_version = ?)",
sourceID,
expectedConfigVersion,
).
Updates(updates)
return result.RowsAffected, result.Error
}
// RenewPagesSourceRuntimeLease extends an active lease held by the given token.
func RenewPagesSourceRuntimeLease(
ctx context.Context,
sourceID uint,
token string,
now time.Time,
expiresAt time.Time,
) (int64, error) {
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ? AND lease_token = ? AND lease_expires_at > ?", sourceID, token, now).
Updates(map[string]any{"lease_expires_at": expiresAt})
return result.RowsAffected, result.Error
}
// UpdatePagesSourceRuntimeByActiveLease updates runtime while the caller still owns the lease.
func UpdatePagesSourceRuntimeByActiveLease(
ctx context.Context,
sourceID uint,
token string,
now time.Time,
updates map[string]any,
) (int64, error) {
return UpdatePagesSourceRuntimeByActiveLeaseTx(db.DB(ctx), sourceID, token, now, updates)
}
// UpdatePagesSourceRuntimeByActiveLeaseTx updates runtime under an active lease inside a transaction.
func UpdatePagesSourceRuntimeByActiveLeaseTx(
tx *gorm.DB,
sourceID uint,
token string,
now time.Time,
updates map[string]any,
) (int64, error) {
result := tx.Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ? AND lease_token = ? AND lease_expires_at > ?", sourceID, token, now).
Updates(updates)
return result.RowsAffected, result.Error
}
// RecoverExpiredPagesSourceRuntimeLease clears one exact expired lease owner.
func RecoverExpiredPagesSourceRuntimeLease(
ctx context.Context,
sourceID uint,
token string,
expiresAt time.Time,
status string,
now time.Time,
updates map[string]any,
) (int64, error) {
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ?", sourceID).
Where("lease_token = ?", token).
Where("lease_expires_at = ? AND lease_expires_at <= ?", expiresAt, now).
Where("sync_status = ?", status).
Updates(updates)
return result.RowsAffected, result.Error
}
// MarkPagesSourceInitialCheckDispatchFailed marks runtime failed when config still matches and lease is free.
func MarkPagesSourceInitialCheckDispatchFailed(
ctx context.Context,
sourceID uint,
configVersion int,
now time.Time,
updates map[string]any,
) (int64, error) {
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ?", sourceID).
Where("lease_expires_at IS NULL OR lease_expires_at <= ?", now).
Where(
"EXISTS (SELECT 1 FROM of_pages_project_sources source WHERE source.id = ? AND source.config_version = ?)",
sourceID,
configVersion,
).
Updates(updates)
return result.RowsAffected, result.Error
}
// RecordPagesSourceAutoDispatchFailure records a failed auto-sync dispatch while status still matches.
func RecordPagesSourceAutoDispatchFailure(
ctx context.Context,
sourceID uint,
configVersion int,
sourceType string,
releaseSelector string,
revision string,
updateAvailableStatus string,
now time.Time,
updates map[string]any,
) (int64, error) {
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ?", sourceID).
Where("sync_status = ? AND last_seen_revision = ?", updateAvailableStatus, revision).
Where("lease_expires_at IS NULL OR lease_expires_at <= ?", now).
Where(`EXISTS (
SELECT 1 FROM of_pages_project_sources AS source
WHERE source.id = ? AND source.config_version = ?
AND source.source_type = ? AND source.release_selector = ?
AND source.auto_update_enabled = ?
)`,
sourceID,
configVersion,
sourceType,
releaseSelector,
true,
).
Updates(updates)
return result.RowsAffected, result.Error
}
// ListExpiredPagesSourceLeaseCandidates returns expired checking/syncing leases for recovery.
func ListExpiredPagesSourceLeaseCandidates(
ctx context.Context,
now time.Time,
syncStatuses []string,
) ([]model.PagesExpiredSourceLeaseCandidate, error) {
var candidates []model.PagesExpiredSourceLeaseCandidate
err := db.DB(ctx).
Table("of_pages_project_source_runtime AS runtime").
Select(`runtime.source_id, runtime.lease_token, runtime.lease_expires_at,
runtime.sync_status, source.source_type, source.release_selector`).
Joins("JOIN of_pages_project_sources AS source ON source.id = runtime.source_id").
Where("runtime.lease_token <> ''").
Where("runtime.lease_expires_at IS NOT NULL AND runtime.lease_expires_at <= ?", now).
Where("runtime.sync_status IN ?", syncStatuses).
Order("runtime.source_id ASC").
Scan(&candidates).Error
if err != nil {
return nil, err
}
return candidates, nil
}
// CountDueGitHubPagesSourceChecks counts due latest GitHub sources.
func CountDueGitHubPagesSourceChecks(
ctx context.Context,
now time.Time,
sourceType string,
releaseSelector string,
) (int64, error) {
var count int64
err := dueGitHubPagesSourceQuery(ctx, now, sourceType, releaseSelector).Count(&count).Error
return count, err
}
// ListDueGitHubPagesSourceChecks lists a batch of due latest GitHub sources in stable order.
func ListDueGitHubPagesSourceChecks(
ctx context.Context,
now time.Time,
sourceType string,
releaseSelector string,
limit int,
) ([]model.PagesDueGitHubSourceCandidate, error) {
var candidates []model.PagesDueGitHubSourceCandidate
err := dueGitHubPagesSourceQuery(ctx, now, sourceType, releaseSelector).
Select("source.id AS source_id, source.config_version").
Order("runtime.next_check_at ASC").
Order("source.id ASC").
Limit(limit).
Scan(&candidates).Error
if err != nil {
return nil, err
}
return candidates, nil
}
func dueGitHubPagesSourceQuery(
ctx context.Context,
now time.Time,
sourceType string,
releaseSelector string,
) *gorm.DB {
return db.DB(ctx).
Table("of_pages_project_source_runtime AS runtime").
Joins("JOIN of_pages_project_sources AS source ON source.id = runtime.source_id").
Where("source.source_type = ?", sourceType).
Where("source.release_selector = ?", releaseSelector).
Where("runtime.next_check_at IS NOT NULL AND runtime.next_check_at <= ?", now)
}
// GetPagesDeploymentBySourceRevision loads a deployment by project source identity and revision.
func GetPagesDeploymentBySourceRevision(
ctx context.Context,
projectID uint,
sourceIdentity string,
revision string,
) (*model.PagesDeployment, error) {
var deployment model.PagesDeployment
err := db.DB(ctx).
Where("project_id = ? AND source_identity = ? AND source_revision = ?", projectID, sourceIdentity, revision).
First(&deployment).Error
if err != nil {
return nil, err
}
return &deployment, nil
}