Files
license/internal/issuance/repository.go
2026-07-31 16:59:55 +08:00

292 lines
9.4 KiB
Go

package issuance
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgconn"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"git.apinb.com/ops/license/sdk/licence"
)
type Repository struct {
db *gorm.DB
}
type issuanceRow struct {
ID uuid.UUID `gorm:"column:id;type:uuid;primaryKey"`
SubjectID uuid.UUID `gorm:"column:subject_id;type:uuid"`
IssuanceType string `gorm:"column:issuance_type"`
PreviousLicenceID *uuid.UUID `gorm:"column:previous_licence_id;type:uuid"`
ValidFrom time.Time `gorm:"column:valid_from"`
ExpiresOn time.Time `gorm:"column:expires_on"`
MaxDatabase int64 `gorm:"column:max_database"`
MaxMiddleware int64 `gorm:"column:max_middleware"`
MaxNetworkDevice int64 `gorm:"column:max_network_device"`
MaxSecurity int64 `gorm:"column:max_security"`
MaxStorage int64 `gorm:"column:max_storage"`
MaxPC int64 `gorm:"column:max_pc"`
MaxServer int64 `gorm:"column:max_server"`
MaxUser int64 `gorm:"column:max_user"`
MaxRole int64 `gorm:"column:max_role"`
MaxPermission int64 `gorm:"column:max_permission"`
MaxMenu int64 `gorm:"column:max_menu"`
SigningKeyID uuid.UUID `gorm:"column:signing_key_id;type:uuid"`
PayloadJSON []byte `gorm:"column:payload_json;type:jsonb"`
FileContent []byte `gorm:"column:file_content"`
IssuedAt time.Time `gorm:"column:issued_at"`
OperatorName string `gorm:"column:operator_name"`
}
type summaryRow struct {
ID uuid.UUID `gorm:"column:id"`
SubjectID uuid.UUID `gorm:"column:subject_id"`
PlatformName string `gorm:"column:platform_name"`
Workspace string `gorm:"column:workspace"`
IssuanceType string `gorm:"column:issuance_type"`
PreviousLicenceID *uuid.UUID `gorm:"column:previous_licence_id"`
ValidFrom time.Time `gorm:"column:valid_from"`
ExpiresOn time.Time `gorm:"column:expires_on"`
IssuedAt time.Time `gorm:"column:issued_at"`
}
type downloadRow struct {
ID uuid.UUID `gorm:"column:id"`
PlatformName string `gorm:"column:platform_name"`
Workspace string `gorm:"column:workspace"`
FileContent []byte `gorm:"column:file_content"`
}
func (issuanceRow) TableName() string {
return "licence_issuances"
}
func NewRepository(db *gorm.DB) (*Repository, error) {
if db == nil {
return nil, ErrInvalidInput
}
return &Repository{db: db}, nil
}
func (r *Repository) Insert(ctx context.Context, tx *gorm.DB, record Record) error {
if r == nil || r.db == nil || ctx == nil || tx == nil {
return ErrInvalidInput
}
payload, err := json.Marshal(record.Payload)
if err != nil {
return err
}
row := rowFromRecord(record, payload)
if err := tx.WithContext(ctx).Create(&row).Error; err != nil {
return mapDatabaseError(err)
}
return nil
}
func (r *Repository) Get(ctx context.Context, id uuid.UUID) (Record, error) {
var row issuanceRow
err := r.db.WithContext(ctx).Where("id = ?", id).Take(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return Record{}, ErrNotFound
}
if err != nil {
return Record{}, err
}
return row.record()
}
func (r *Repository) List(ctx context.Context, filter Filter, page, pageSize int) ([]RecordSummary, int64, error) {
query := r.db.WithContext(ctx).
Table("licence_issuances AS issuances").
Joins("JOIN authorization_subjects AS subjects ON subjects.id = issuances.subject_id")
if filter.PlatformName != "" {
query = query.Where("STRPOS(LOWER(subjects.platform_name), LOWER(?)) > 0", filter.PlatformName)
}
if filter.Workspace != "" {
query = query.Where("STRPOS(LOWER(subjects.workspace), LOWER(?)) > 0", filter.Workspace)
}
if filter.ValidFrom != nil {
query = query.Where("issuances.valid_from = ?", *filter.ValidFrom)
}
if filter.ExpiresOn != nil {
query = query.Where("issuances.expires_on = ?", *filter.ExpiresOn)
}
if filter.IssuedOn != nil {
query = query.Where("issuances.issued_at >= ? AND issuances.issued_at < ?", *filter.IssuedOn, filter.IssuedOn.AddDate(0, 0, 1))
}
return listSummaries(query, page, pageSize)
}
func (r *Repository) ListBySubject(ctx context.Context, subjectID uuid.UUID, page, pageSize int) ([]RecordSummary, int64, error) {
query := r.db.WithContext(ctx).
Table("licence_issuances AS issuances").
Joins("JOIN authorization_subjects AS subjects ON subjects.id = issuances.subject_id").
Where("issuances.subject_id = ?", subjectID)
return listSummaries(query, page, pageSize)
}
func (r *Repository) Download(ctx context.Context, id uuid.UUID) ([]byte, DownloadMetadata, error) {
var row downloadRow
err := r.db.WithContext(ctx).
Table("licence_issuances AS issuances").
Select("issuances.id, subjects.platform_name, subjects.workspace, issuances.file_content").
Joins("JOIN authorization_subjects AS subjects ON subjects.id = issuances.subject_id").
Where("issuances.id = ?", id).
Take(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, DownloadMetadata{}, ErrNotFound
}
if err != nil {
return nil, DownloadMetadata{}, err
}
return cloneBytes(row.FileContent), DownloadMetadata{
ID: row.ID,
PlatformName: row.PlatformName,
Workspace: row.Workspace,
}, nil
}
func (r *Repository) lock(ctx context.Context, tx *gorm.DB, id uuid.UUID) (Record, error) {
var row issuanceRow
err := tx.WithContext(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", id).
Take(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return Record{}, ErrNotFound
}
if err != nil {
return Record{}, err
}
return row.record()
}
func listSummaries(query *gorm.DB, page, pageSize int) ([]RecordSummary, int64, error) {
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []summaryRow
err := query.
Select(`issuances.id, issuances.subject_id, subjects.platform_name, subjects.workspace,
issuances.issuance_type, issuances.previous_licence_id, issuances.valid_from,
issuances.expires_on, issuances.issued_at`).
Order("issuances.issued_at DESC, issuances.id DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&rows).Error
if err != nil {
return nil, 0, err
}
items := make([]RecordSummary, len(rows))
for index := range rows {
items[index] = rows[index].summary()
}
return items, total, nil
}
func rowFromRecord(record Record, payload []byte) issuanceRow {
return issuanceRow{
ID: record.ID,
SubjectID: record.SubjectID,
IssuanceType: record.IssuanceType,
PreviousLicenceID: cloneUUID(record.PreviousLicenceID),
ValidFrom: record.ValidFrom,
ExpiresOn: record.ExpiresOn,
MaxDatabase: record.Quotas.MaxDatabase,
MaxMiddleware: record.Quotas.MaxMiddleware,
MaxNetworkDevice: record.Quotas.MaxNetworkDevice,
MaxSecurity: record.Quotas.MaxSecurity,
MaxStorage: record.Quotas.MaxStorage,
MaxPC: record.Quotas.MaxPC,
MaxServer: record.Quotas.MaxServer,
MaxUser: record.Quotas.MaxUser,
MaxRole: record.Quotas.MaxRole,
MaxPermission: record.Quotas.MaxPermission,
MaxMenu: record.Quotas.MaxMenu,
SigningKeyID: record.SigningKeyID,
PayloadJSON: cloneBytes(payload),
FileContent: cloneBytes(record.FileContent),
IssuedAt: record.IssuedAt,
OperatorName: record.OperatorName,
}
}
func (row issuanceRow) record() (Record, error) {
var payload licence.Claims
if err := json.Unmarshal(row.PayloadJSON, &payload); err != nil {
return Record{}, err
}
return Record{
ID: row.ID,
SubjectID: row.SubjectID,
IssuanceType: row.IssuanceType,
PreviousLicenceID: cloneUUID(row.PreviousLicenceID),
ValidFrom: row.ValidFrom,
ExpiresOn: row.ExpiresOn,
Quotas: licence.Quotas{
MaxDatabase: row.MaxDatabase,
MaxMiddleware: row.MaxMiddleware,
MaxNetworkDevice: row.MaxNetworkDevice,
MaxSecurity: row.MaxSecurity,
MaxStorage: row.MaxStorage,
MaxPC: row.MaxPC,
MaxServer: row.MaxServer,
MaxUser: row.MaxUser,
MaxRole: row.MaxRole,
MaxPermission: row.MaxPermission,
MaxMenu: row.MaxMenu,
},
SigningKeyID: row.SigningKeyID,
Payload: payload,
FileContent: cloneBytes(row.FileContent),
IssuedAt: row.IssuedAt,
OperatorName: row.OperatorName,
}, nil
}
func (row summaryRow) summary() RecordSummary {
return RecordSummary{
ID: row.ID,
SubjectID: row.SubjectID,
PlatformName: row.PlatformName,
Workspace: row.Workspace,
IssuanceType: row.IssuanceType,
PreviousLicenceID: cloneUUID(row.PreviousLicenceID),
ValidFrom: row.ValidFrom,
ExpiresOn: row.ExpiresOn,
IssuedAt: row.IssuedAt,
}
}
func mapDatabaseError(err error) error {
var postgresError *pgconn.PgError
if errors.As(err, &postgresError) && postgresError.Code == "23503" &&
postgresError.ConstraintName == "licence_issuances_previous_subject_fkey" {
return ErrPreviousMismatch
}
return err
}
func cloneBytes(value []byte) []byte {
if value == nil {
return nil
}
result := make([]byte, len(value))
copy(result, value)
return result
}
func cloneUUID(value *uuid.UUID) *uuid.UUID {
if value == nil {
return nil
}
result := *value
return &result
}