Bootstrap independently reviewed catalogs through audited import, approval, and initial activation transitions, then resume insight scheduling safely.
188 lines
6.7 KiB
Go
188 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
|
|
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/config"
|
|
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/database"
|
|
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/eventcatalog"
|
|
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/insight"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type options struct {
|
|
action string
|
|
filePath string
|
|
version string
|
|
actor string
|
|
reviewer string
|
|
currentJalaliYear int
|
|
}
|
|
|
|
func main() {
|
|
os.Exit(realMain(os.Args[1:]))
|
|
}
|
|
|
|
func realMain(arguments []string) int {
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
defer cancel()
|
|
if err := run(ctx, arguments, logger); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
fmt.Fprint(os.Stdout, calendarUsage)
|
|
return 0
|
|
}
|
|
logger.Error("calendar command failed", "error", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
const calendarUsage = `Usage: calendar -action <import|approve|activate|bootstrap> [options]
|
|
|
|
-file PATH reviewed JSON catalog for import or bootstrap
|
|
-current-jalali-year YEAR current Jalali year for coverage validation
|
|
-version VERSION catalog version for approve or activate
|
|
-reviewer ID independent reviewer identity for bootstrap
|
|
-actor ID approval or activation operator identity
|
|
`
|
|
|
|
func run(ctx context.Context, arguments []string, logger *slog.Logger) error {
|
|
command, err := parseOptions(arguments)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var input eventcatalog.Import
|
|
if command.action == "import" || command.action == "bootstrap" {
|
|
input, err = readImport(command.filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := eventcatalog.ValidateImport(input, command.currentJalaliYear); err != nil {
|
|
return fmt.Errorf("validating calendar catalog: %w", err)
|
|
}
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("loading configuration: %w", err)
|
|
}
|
|
pool, err := database.Open(ctx, cfg.DatabaseURL)
|
|
if err != nil {
|
|
return fmt.Errorf("connecting to database: %w", err)
|
|
}
|
|
defer pool.Close()
|
|
repository := eventcatalog.NewRepository(pool)
|
|
|
|
switch command.action {
|
|
case "import":
|
|
if err := repository.ImportDraft(ctx, input); err != nil {
|
|
return fmt.Errorf("importing calendar catalog: %w", err)
|
|
}
|
|
logger.Info("calendar draft imported", "version", input.Version, "events", len(input.Events))
|
|
case "approve":
|
|
if err := repository.Approve(ctx, command.version, command.actor); err != nil {
|
|
return fmt.Errorf("approving calendar catalog: %w", err)
|
|
}
|
|
logger.Info("calendar approved", "version", command.version, "reviewer", command.actor)
|
|
case "activate":
|
|
if err := repository.Activate(ctx, command.version, command.actor); err != nil {
|
|
return fmt.Errorf("activating calendar catalog: %w", err)
|
|
}
|
|
created, err := scheduleInsights(ctx, pool)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger.Info("calendar activated", "version", command.version, "actor", command.actor, "created_jobs", created)
|
|
case "bootstrap":
|
|
result, err := eventcatalog.BootstrapCatalog(ctx, repository, input, command.currentJalaliYear, command.reviewer, command.actor, func(ctx context.Context) (int, error) {
|
|
return scheduleInsights(ctx, pool)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("bootstrapping calendar catalog: %w", err)
|
|
}
|
|
logger.Info("calendar bootstrap complete", "version", result.Version, "status", result.Status, "transitions", result.Transitions, "no_op", result.NoOp, "created_jobs", result.CreatedJobs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseOptions(arguments []string) (options, error) {
|
|
var result options
|
|
flags := flag.NewFlagSet("calendar", flag.ContinueOnError)
|
|
flags.SetOutput(io.Discard)
|
|
flags.StringVar(&result.action, "action", "", "import, approve, activate, or bootstrap")
|
|
flags.StringVar(&result.filePath, "file", "", "reviewed JSON catalog for import or bootstrap")
|
|
flags.StringVar(&result.version, "version", "", "catalog version for approve or activate")
|
|
flags.StringVar(&result.actor, "actor", "", "audited reviewer or activation operator identity")
|
|
flags.StringVar(&result.reviewer, "reviewer", "", "independent reviewer identity for bootstrap")
|
|
flags.IntVar(&result.currentJalaliYear, "current-jalali-year", 0, "current Jalali year used for coverage validation")
|
|
if err := flags.Parse(arguments); err != nil {
|
|
return options{}, fmt.Errorf("parsing calendar arguments: %w", err)
|
|
}
|
|
if flags.NArg() != 0 {
|
|
return options{}, errors.New("calendar: positional arguments are not supported")
|
|
}
|
|
result.action = strings.TrimSpace(result.action)
|
|
result.filePath = strings.TrimSpace(result.filePath)
|
|
result.version = strings.TrimSpace(result.version)
|
|
result.actor = strings.TrimSpace(result.actor)
|
|
result.reviewer = strings.TrimSpace(result.reviewer)
|
|
switch result.action {
|
|
case "import":
|
|
if result.filePath == "" || result.currentJalaliYear == 0 {
|
|
return options{}, errors.New("calendar: file and current-jalali-year are required for import")
|
|
}
|
|
case "approve", "activate":
|
|
if result.version == "" || result.actor == "" {
|
|
return options{}, fmt.Errorf("calendar: version and actor are required for %s", result.action)
|
|
}
|
|
case "bootstrap":
|
|
if result.filePath == "" || result.currentJalaliYear == 0 || result.reviewer == "" || result.actor == "" {
|
|
return options{}, errors.New("calendar: file, current-jalali-year, reviewer, and actor are required for bootstrap")
|
|
}
|
|
default:
|
|
return options{}, errors.New("calendar: action must be import, approve, activate, or bootstrap")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func readImport(path string) (eventcatalog.Import, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return eventcatalog.Import{}, fmt.Errorf("opening calendar catalog: %w", err)
|
|
}
|
|
defer file.Close()
|
|
var input eventcatalog.Import
|
|
decoder := json.NewDecoder(file)
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&input); err != nil {
|
|
return eventcatalog.Import{}, fmt.Errorf("decoding calendar catalog: %w", err)
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
|
if err == nil {
|
|
return eventcatalog.Import{}, errors.New("decoding calendar catalog: trailing JSON document")
|
|
}
|
|
return eventcatalog.Import{}, fmt.Errorf("decoding calendar catalog trailing data: %w", err)
|
|
}
|
|
return input, nil
|
|
}
|
|
|
|
func scheduleInsights(ctx context.Context, pool *pgxpool.Pool) (int, error) {
|
|
service := insight.NewService(insight.NewRepository(pool), insight.NewPostgreSQLBuilder(pool))
|
|
created, err := service.EnqueueAll(ctx, "CALENDAR")
|
|
if err != nil {
|
|
return 0, fmt.Errorf("enqueueing calendar insights: %w", err)
|
|
}
|
|
return created, nil
|
|
}
|