From f7af19b0dc8e2167c8a3be76d6b4be3f0baaf28f Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Wed, 2 Apr 2025 13:23:59 -0700
Subject: [PATCH] submissions: naively implement operations

---
 pkg/datastore/datastore.go            |  8 ++++
 pkg/datastore/gormstore/db.go         |  1 +
 pkg/datastore/gormstore/gormstore.go  |  4 ++
 pkg/datastore/gormstore/operations.go | 55 +++++++++++++++++++++++++++
 pkg/model/operation.go                | 19 +++++++++
 pkg/service/operations.go             | 44 +++++++++++++++++++++
 6 files changed, 131 insertions(+)
 create mode 100644 pkg/datastore/gormstore/operations.go
 create mode 100644 pkg/model/operation.go
 create mode 100644 pkg/service/operations.go

diff --git a/pkg/datastore/datastore.go b/pkg/datastore/datastore.go
index 1cc3f48..82eda8f 100644
--- a/pkg/datastore/datastore.go
+++ b/pkg/datastore/datastore.go
@@ -23,6 +23,7 @@ const (
 
 type Datastore interface {
 	Mapfixes() Mapfixes
+	Operations() Operations
 	Submissions() Submissions
 	Scripts() Scripts
 	ScriptPolicy() ScriptPolicy
@@ -39,6 +40,13 @@ type Mapfixes interface {
 	List(ctx context.Context, filters OptionalMap, page model.Page, sort ListSort) ([]model.Mapfix, error)
 }
 
+type Operations interface {
+	Get(ctx context.Context, id int32) (model.Operation, error)
+	Create(ctx context.Context, smap model.Operation) (model.Operation, error)
+	Update(ctx context.Context, id int32, values OptionalMap) error
+	Delete(ctx context.Context, id int32) error
+}
+
 type Submissions interface {
 	Get(ctx context.Context, id int64) (model.Submission, error)
 	GetList(ctx context.Context, id []int64) ([]model.Submission, error)
diff --git a/pkg/datastore/gormstore/db.go b/pkg/datastore/gormstore/db.go
index 01d001c..e4d0ed3 100644
--- a/pkg/datastore/gormstore/db.go
+++ b/pkg/datastore/gormstore/db.go
@@ -32,6 +32,7 @@ func New(ctx *cli.Context) (datastore.Datastore, error) {
 	if ctx.Bool("migrate") {
 		if err := db.AutoMigrate(
 			&model.Mapfix{},
+			&model.Operation{},
 			&model.Submission{},
 			&model.Script{},
 			&model.ScriptPolicy{},
diff --git a/pkg/datastore/gormstore/gormstore.go b/pkg/datastore/gormstore/gormstore.go
index da16895..7d88b12 100644
--- a/pkg/datastore/gormstore/gormstore.go
+++ b/pkg/datastore/gormstore/gormstore.go
@@ -13,6 +13,10 @@ func (g Gormstore) Mapfixes() datastore.Mapfixes {
 	return &Mapfixes{db: g.db}
 }
 
+func (g Gormstore) Operations() datastore.Operations {
+	return &Operations{db: g.db}
+}
+
 func (g Gormstore) Submissions() datastore.Submissions {
 	return &Submissions{db: g.db}
 }
diff --git a/pkg/datastore/gormstore/operations.go b/pkg/datastore/gormstore/operations.go
new file mode 100644
index 0000000..bf49c86
--- /dev/null
+++ b/pkg/datastore/gormstore/operations.go
@@ -0,0 +1,55 @@
+package gormstore
+
+import (
+	"context"
+	"errors"
+
+	"git.itzana.me/strafesnet/maps-service/pkg/datastore"
+	"git.itzana.me/strafesnet/maps-service/pkg/model"
+	"gorm.io/gorm"
+)
+
+type Operations struct {
+	db *gorm.DB
+}
+
+func (env *Operations) Get(ctx context.Context, id int32) (model.Operation, error) {
+	var operation model.Operation
+	if err := env.db.First(&operation, id).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return operation, datastore.ErrNotExist
+		}
+		return operation, err
+	}
+	return operation, nil
+}
+
+func (env *Operations) Create(ctx context.Context, smap model.Operation) (model.Operation, error) {
+	if err := env.db.Create(&smap).Error; err != nil {
+		return smap, err
+	}
+
+	return smap, nil
+}
+
+func (env *Operations) Update(ctx context.Context, id int32, values datastore.OptionalMap) error {
+	if err := env.db.Model(&model.Operation{}).Where("id = ?", id).Updates(values.Map()).Error; err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return datastore.ErrNotExist
+		}
+		return err
+	}
+
+	return nil
+}
+
+func (env *Operations) Delete(ctx context.Context, id int32) error {
+	if err := env.db.Delete(&model.Operation{}, id).Error; err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return datastore.ErrNotExist
+		}
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/model/operation.go b/pkg/model/operation.go
new file mode 100644
index 0000000..4949a92
--- /dev/null
+++ b/pkg/model/operation.go
@@ -0,0 +1,19 @@
+package model
+
+import "time"
+
+type OperationStatus int32
+const (
+	OperationStatusCreated   OperationStatus = 0
+	OperationStatusCompleted OperationStatus = 1
+	OperationStatusFailed    OperationStatus = 2
+)
+
+type Operation struct {
+	ID            int32 `gorm:"primaryKey"`
+	CreatedAt     time.Time
+	Owner         int64 // UserID
+	StatusID      OperationStatus
+	StatusMessage string
+	Path          string // redirect to view completed operation e.g. "/mapfixes/4"
+}
diff --git a/pkg/service/operations.go b/pkg/service/operations.go
new file mode 100644
index 0000000..eb48adc
--- /dev/null
+++ b/pkg/service/operations.go
@@ -0,0 +1,44 @@
+package service
+
+import (
+	"context"
+
+	"git.itzana.me/strafesnet/maps-service/pkg/api"
+)
+
+// GetOperation implements getOperation operation.
+//
+// Get the specified operation by ID.
+//
+// GET /operations/{OperationID}
+func (svc *Service) GetOperation(ctx context.Context, params api.GetOperationParams) (*api.Operation, error) {
+	userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
+	if !ok {
+		return nil, ErrUserInfo
+	}
+
+	// You must be the operation owner to read it
+
+	operation, err := svc.DB.Operations().Get(ctx, params.OperationID)
+	if err != nil {
+		return nil, err
+	}
+
+	has_role, err := userInfo.IsSubmitter(uint64(operation.Owner))
+	if err != nil {
+		return nil, err
+	}
+	// check if caller is operation owner
+	if !has_role {
+		return nil, ErrPermissionDeniedNotSubmitter
+	}
+
+	return &api.Operation{
+		OperationID:   operation.ID,
+		Date:          operation.CreatedAt.Unix(),
+		Owner:         operation.Owner,
+		Status:        int32(operation.StatusID),
+		StatusMessage: operation.StatusMessage,
+		Path:          operation.Path,
+	}, nil
+}