mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
943818f7d4
将 OpenFlare 与平台业务的数据访问从 model 与 apps 直连迁入 repository, model 仅保留实体与无 IO 规则;补充 code-check 架构守卫与开发规范。
418 lines
13 KiB
Go
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
|
|
}
|