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 }