Implement Maps #241

Merged
Quaternions merged 7 commits from maps into staging 2025-07-24 04:29:01 +00:00
22 changed files with 1921 additions and 1066 deletions

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.22
toolchain go1.23.3
require (
git.itzana.me/strafesnet/go-grpc v0.0.0-20250719033306-150dea07cf00
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815
git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9
github.com/dchest/siphash v1.2.3
github.com/go-faster/errors v0.7.1

4
go.sum
View File

@@ -1,7 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.itzana.me/strafesnet/go-grpc v0.0.0-20250719033306-150dea07cf00 h1:L9HiJzMrXCKZhZz+RyUr5RXdMcGQMU85aap6L+QQDZ0=
git.itzana.me/strafesnet/go-grpc v0.0.0-20250719033306-150dea07cf00/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815 h1:hkuOnehphRXUq/2z2UYgoqTq5MJj1GsWfshyc7bXda8=
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9 h1:7lU6jyR7S7Rhh1dnUp7GyIRHUTBXZagw8F4n4hOyxLw=
git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9/go.mod h1:uyYerSieEt4v0MJCdPLppG0LtJ4Yj035vuTetWGsxjY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

View File

@@ -80,6 +80,21 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/migrate-maps:
post:
summary: Perform maps migration
operationId: migrateMaps
tags:
- Maps
responses:
"200":
description: Successful response
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/maps:
get:
summary: Get list of maps

View File

@@ -301,6 +301,12 @@ type Invoker interface {
//
// GET /submissions
ListSubmissions(ctx context.Context, params ListSubmissionsParams) (*Submissions, error)
// MigrateMaps invokes migrateMaps operation.
//
// Perform maps migration.
//
// POST /migrate-maps
MigrateMaps(ctx context.Context) error
// ReleaseSubmissions invokes releaseSubmissions operation.
//
// Release a set of uploaded maps.
@@ -6121,6 +6127,111 @@ func (c *Client) sendListSubmissions(ctx context.Context, params ListSubmissions
return result, nil
}
// MigrateMaps invokes migrateMaps operation.
//
// Perform maps migration.
//
// POST /migrate-maps
func (c *Client) MigrateMaps(ctx context.Context) error {
_, err := c.sendMigrateMaps(ctx)
return err
}
func (c *Client) sendMigrateMaps(ctx context.Context) (res *MigrateMapsOK, err error) {
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("migrateMaps"),
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/migrate-maps"),
}
// Run stopwatch.
startTime := time.Now()
defer func() {
// Use floating point division here for higher precision (instead of Millisecond method).
elapsedDuration := time.Since(startTime)
c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...))
}()
// Increment request counter.
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
// Start a span for this request.
ctx, span := c.cfg.Tracer.Start(ctx, MigrateMapsOperation,
trace.WithAttributes(otelAttrs...),
clientSpanKind,
)
// Track stage for error reporting.
var stage string
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, stage)
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
}
span.End()
}()
stage = "BuildURL"
u := uri.Clone(c.requestURL(ctx))
var pathParts [1]string
pathParts[0] = "/migrate-maps"
uri.AddPathParts(u, pathParts[:]...)
stage = "EncodeRequest"
r, err := ht.NewRequest(ctx, "POST", u)
if err != nil {
return res, errors.Wrap(err, "create request")
}
{
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, MigrateMapsOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
}
}
if ok := func() bool {
nextRequirement:
for _, requirement := range []bitset{
{0b00000001},
} {
for i, mask := range requirement {
if satisfied[i]&mask != mask {
continue nextRequirement
}
}
return true
}
return false
}(); !ok {
return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied
}
}
stage = "SendRequest"
resp, err := c.cfg.Client.Do(r)
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
stage = "DecodeResponse"
result, err := decodeMigrateMapsResponse(resp)
if err != nil {
return res, errors.Wrap(err, "decode response")
}
return result, nil
}
// ReleaseSubmissions invokes releaseSubmissions operation.
//
// Release a set of uploaded maps.

View File

@@ -8433,6 +8433,186 @@ func (s *Server) handleListSubmissionsRequest(args [0]string, argsEscaped bool,
}
}
// handleMigrateMapsRequest handles migrateMaps operation.
//
// Perform maps migration.
//
// POST /migrate-maps
func (s *Server) handleMigrateMapsRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) {
statusWriter := &codeRecorder{ResponseWriter: w}
w = statusWriter
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("migrateMaps"),
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/migrate-maps"),
}
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), MigrateMapsOperation,
trace.WithAttributes(otelAttrs...),
serverSpanKind,
)
defer span.End()
// Add Labeler to context.
labeler := &Labeler{attrs: otelAttrs}
ctx = contextWithLabeler(ctx, labeler)
// Run stopwatch.
startTime := time.Now()
defer func() {
elapsedDuration := time.Since(startTime)
attrSet := labeler.AttributeSet()
attrs := attrSet.ToSlice()
code := statusWriter.status
if code != 0 {
codeAttr := semconv.HTTPResponseStatusCode(code)
attrs = append(attrs, codeAttr)
span.SetAttributes(codeAttr)
}
attrOpt := metric.WithAttributes(attrs...)
// Increment request counter.
s.requests.Add(ctx, 1, attrOpt)
// Use floating point division here for higher precision (instead of Millisecond method).
s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt)
}()
var (
recordError = func(stage string, err error) {
span.RecordError(err)
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
// Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges,
// unless there was another error (e.g., network error receiving the response body; or 3xx codes with
// max redirects exceeded), in which case status MUST be set to Error.
code := statusWriter.status
if code >= 100 && code < 500 {
span.SetStatus(codes.Error, stage)
}
attrSet := labeler.AttributeSet()
attrs := attrSet.ToSlice()
if code != 0 {
attrs = append(attrs, semconv.HTTPResponseStatusCode(code))
}
s.errors.Add(ctx, 1, metric.WithAttributes(attrs...))
}
err error
opErrContext = ogenerrors.OperationContext{
Name: MigrateMapsOperation,
ID: "migrateMaps",
}
)
{
type bitset = [1]uint8
var satisfied bitset
{
sctx, ok, err := s.securityCookieAuth(ctx, MigrateMapsOperation, r)
if err != nil {
err = &ogenerrors.SecurityError{
OperationContext: opErrContext,
Security: "CookieAuth",
Err: err,
}
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
defer recordError("Security:CookieAuth", err)
}
return
}
if ok {
satisfied[0] |= 1 << 0
ctx = sctx
}
}
if ok := func() bool {
nextRequirement:
for _, requirement := range []bitset{
{0b00000001},
} {
for i, mask := range requirement {
if satisfied[i]&mask != mask {
continue nextRequirement
}
}
return true
}
return false
}(); !ok {
err = &ogenerrors.SecurityError{
OperationContext: opErrContext,
Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied,
}
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
defer recordError("Security", err)
}
return
}
}
var response *MigrateMapsOK
if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{
Context: ctx,
OperationName: MigrateMapsOperation,
OperationSummary: "Perform maps migration",
OperationID: "migrateMaps",
Body: nil,
Params: middleware.Parameters{},
Raw: r,
}
type (
Request = struct{}
Params = struct{}
Response = *MigrateMapsOK
)
response, err = middleware.HookMiddleware[
Request,
Params,
Response,
](
m,
mreq,
nil,
func(ctx context.Context, request Request, params Params) (response Response, err error) {
err = s.h.MigrateMaps(ctx)
return response, err
},
)
} else {
err = s.h.MigrateMaps(ctx)
}
if err != nil {
if errRes, ok := errors.Into[*ErrorStatusCode](err); ok {
if err := encodeErrorResponse(errRes, w, span); err != nil {
defer recordError("Internal", err)
}
return
}
if errors.Is(err, ht.ErrNotImplemented) {
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil {
defer recordError("Internal", err)
}
return
}
if err := encodeMigrateMapsResponse(response, w, span); err != nil {
defer recordError("EncodeResponse", err)
if !errors.Is(err, ht.ErrInternalServerErrorResponse) {
s.cfg.ErrorHandler(ctx, w, r, err)
}
return
}
}
// handleReleaseSubmissionsRequest handles releaseSubmissions operation.
//
// Release a set of uploaded maps.

View File

@@ -51,6 +51,7 @@ const (
ListScriptsOperation OperationName = "ListScripts"
ListSubmissionAuditEventsOperation OperationName = "ListSubmissionAuditEvents"
ListSubmissionsOperation OperationName = "ListSubmissions"
MigrateMapsOperation OperationName = "MigrateMaps"
ReleaseSubmissionsOperation OperationName = "ReleaseSubmissions"
SessionRolesOperation OperationName = "SessionRoles"
SessionUserOperation OperationName = "SessionUser"

View File

@@ -3595,6 +3595,66 @@ func decodeListSubmissionsResponse(resp *http.Response) (res *Submissions, _ err
return res, errors.Wrap(defRes, "error")
}
func decodeMigrateMapsResponse(resp *http.Response) (res *MigrateMapsOK, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
return &MigrateMapsOK{}, nil
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeReleaseSubmissionsResponse(resp *http.Response) (res *ReleaseSubmissionsCreated, _ error) {
switch resp.StatusCode {
case 201:

View File

@@ -484,6 +484,13 @@ func encodeListSubmissionsResponse(response *Submissions, w http.ResponseWriter,
return nil
}
func encodeMigrateMapsResponse(response *MigrateMapsOK, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
return nil
}
func encodeReleaseSubmissionsResponse(response *ReleaseSubmissionsCreated, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(201)
span.SetStatus(codes.Ok, http.StatusText(201))

File diff suppressed because it is too large Load Diff

View File

@@ -587,6 +587,9 @@ func (s *Mapfixes) SetMapfixes(val []Mapfix) {
s.Mapfixes = val
}
// MigrateMapsOK is response for MigrateMaps operation.
type MigrateMapsOK struct{}
// Ref: #/components/schemas/Operation
type Operation struct {
OperationID int32 `json:"OperationID"`

View File

@@ -67,6 +67,7 @@ var operationRolesCookieAuth = map[string][]string{
DeleteScriptPolicyOperation: []string{},
DownloadMapAssetOperation: []string{},
GetOperationOperation: []string{},
MigrateMapsOperation: []string{},
ReleaseSubmissionsOperation: []string{},
SessionRolesOperation: []string{},
SessionUserOperation: []string{},

View File

@@ -280,6 +280,12 @@ type Handler interface {
//
// GET /submissions
ListSubmissions(ctx context.Context, params ListSubmissionsParams) (*Submissions, error)
// MigrateMaps implements migrateMaps operation.
//
// Perform maps migration.
//
// POST /migrate-maps
MigrateMaps(ctx context.Context) error
// ReleaseSubmissions implements releaseSubmissions operation.
//
// Release a set of uploaded maps.

View File

@@ -420,6 +420,15 @@ func (UnimplementedHandler) ListSubmissions(ctx context.Context, params ListSubm
return r, ht.ErrNotImplemented
}
// MigrateMaps implements migrateMaps operation.
//
// Perform maps migration.
//
// POST /migrate-maps
func (UnimplementedHandler) MigrateMaps(ctx context.Context) error {
return ht.ErrNotImplemented
}
// ReleaseSubmissions implements releaseSubmissions operation.
//
// Release a set of uploaded maps.

View File

@@ -7,9 +7,11 @@ import (
"git.itzana.me/strafesnet/go-grpc/auth"
"git.itzana.me/strafesnet/go-grpc/maps"
"git.itzana.me/strafesnet/go-grpc/maps_extended"
"git.itzana.me/strafesnet/go-grpc/users"
"git.itzana.me/strafesnet/go-grpc/validator"
"git.itzana.me/strafesnet/maps-service/pkg/api"
"git.itzana.me/strafesnet/maps-service/pkg/controller"
"git.itzana.me/strafesnet/maps-service/pkg/datastore/gormstore"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
"git.itzana.me/strafesnet/maps-service/pkg/service"
@@ -162,6 +164,9 @@ func serve(ctx *cli.Context) error {
grpcServer := grpc.NewServer()
maps_controller := controller.NewMapsController(&svc_inner)
maps_extended.RegisterMapsServiceServer(grpcServer,&maps_controller)
mapfix_controller := validator_controller.NewMapfixesController(&svc_inner)
operation_controller := validator_controller.NewOperationsController(&svc_inner)
script_controller := validator_controller.NewScriptsController(&svc_inner)

186
pkg/controller/maps.go Normal file
View File

@@ -0,0 +1,186 @@
package controller
import (
"context"
"time"
"git.itzana.me/strafesnet/go-grpc/maps_extended"
"git.itzana.me/strafesnet/maps-service/pkg/model"
"git.itzana.me/strafesnet/maps-service/pkg/service"
)
type Maps struct {
*maps_extended.UnimplementedMapsServiceServer
inner *service.Service
}
func NewMapsController(
inner *service.Service,
) Maps {
return Maps{
inner: inner,
}
}
func (svc *Maps) Create(ctx context.Context, request *maps_extended.MapCreate) (*maps_extended.MapId, error) {
id, err := svc.inner.CreateMap(ctx, model.Map{
ID: request.ID,
DisplayName: request.DisplayName,
Creator: request.Creator,
GameID: int32(request.GameID),
Submitter: int64(request.Submitter),
Date: time.Unix(request.Date, 0),
Thumbnail: int64(request.Thumbnail),
AssetVersion: int64(request.AssetVersion),
LoadCount: 0,
Modes: request.Modes,
})
if err != nil {
return nil, err
}
return &maps_extended.MapId{
ID: id,
}, nil
}
func (svc *Maps) Delete(ctx context.Context, request *maps_extended.MapId) (*maps_extended.NullResponse, error) {
err := svc.inner.DeleteMap(ctx, request.ID)
if err != nil {
return nil, err
}
return &maps_extended.NullResponse{}, nil
}
func (svc *Maps) Get(ctx context.Context, request *maps_extended.MapId) (*maps_extended.MapResponse, error) {
item, err := svc.inner.GetMap(ctx, request.ID)
if err != nil {
return nil, err
}
return &maps_extended.MapResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
Date: item.Date.Unix(),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
Thumbnail: uint64(item.Thumbnail),
AssetVersion: uint64(item.AssetVersion),
LoadCount: item.LoadCount,
Modes: item.Modes,
}, nil
}
func (svc *Maps) GetList(ctx context.Context, request *maps_extended.MapIdList) (*maps_extended.MapList, error) {
items, err := svc.inner.GetMapList(ctx, request.ID)
if err != nil {
return nil, err
}
resp := maps_extended.MapList{}
resp.Maps = make([]*maps_extended.MapResponse, len(items))
for i, item := range items {
resp.Maps[i] = &maps_extended.MapResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
Date: item.Date.Unix(),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
Thumbnail: uint64(item.Thumbnail),
AssetVersion: uint64(item.AssetVersion),
LoadCount: item.LoadCount,
Modes: item.Modes,
}
}
return &resp, nil
}
func (svc *Maps) List(ctx context.Context, request *maps_extended.ListRequest) (*maps_extended.MapList, error) {
filter := service.NewMapFilter()
if request.Filter.DisplayName != nil {
filter.SetDisplayName(*request.Filter.DisplayName)
}
if request.Filter.Creator != nil {
filter.SetCreator(*request.Filter.Creator)
}
if request.Filter.GameID != nil {
filter.SetGameID(*request.Filter.GameID)
}
if request.Filter.Submitter != nil {
filter.SetSubmitter(*request.Filter.Submitter)
}
items, err := svc.inner.ListMaps(ctx, filter, model.Page{
Number: int32(request.Page.Number),
Size: int32(request.Page.Size),
})
if err != nil {
return nil, err
}
resp := maps_extended.MapList{}
resp.Maps = make([]*maps_extended.MapResponse, len(items))
for i, item := range items {
resp.Maps[i] = &maps_extended.MapResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
Date: item.Date.Unix(),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
Thumbnail: uint64(item.Thumbnail),
AssetVersion: uint64(item.AssetVersion),
LoadCount: item.LoadCount,
Modes: item.Modes,
}
}
return &resp, nil
}
func (svc *Maps) Update(ctx context.Context, request *maps_extended.MapUpdate) (*maps_extended.NullResponse, error) {
update := service.NewMapUpdate()
if request.DisplayName != nil {
update.SetDisplayName(*request.DisplayName)
}
if request.Creator != nil {
update.SetCreator(*request.Creator)
}
if request.GameID != nil {
update.SetGameID(*request.GameID)
}
if request.Date != nil {
update.SetDate(*request.Date)
}
if request.Submitter != nil {
update.SetSubmitter(*request.Submitter)
}
if request.Thumbnail != nil {
update.SetThumbnail(*request.Thumbnail)
}
if request.AssetVersion != nil {
update.SetAssetVersion(*request.AssetVersion)
}
if request.Modes != nil {
update.SetModes(*request.Modes)
}
err := svc.inner.UpdateMap(ctx, request.ID, update)
if err != nil {
return nil, err
}
return &maps_extended.NullResponse{}, nil
}
func (svc *Maps) IncrementLoadCount(ctx context.Context, request *maps_extended.MapId) (*maps_extended.NullResponse, error) {
err := svc.inner.IncrementMapLoadCount(ctx, request.ID)
if err != nil {
return nil, err
}
return &maps_extended.NullResponse{}, nil
}

View File

@@ -25,6 +25,7 @@ const (
type Datastore interface {
AuditEvents() AuditEvents
Maps() Maps
Mapfixes() Mapfixes
Operations() Operations
Submissions() Submissions
@@ -40,6 +41,16 @@ type AuditEvents interface {
List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.AuditEvent, error)
}
type Maps interface {
Get(ctx context.Context, id int64) (model.Map, error)
GetList(ctx context.Context, id []int64) ([]model.Map, error)
Create(ctx context.Context, smap model.Map) (model.Map, error)
Update(ctx context.Context, id int64, values OptionalMap) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.Map, error)
IncrementLoadCount(ctx context.Context, id int64) error
}
type Mapfixes interface {
Get(ctx context.Context, id int64) (model.Mapfix, error)
GetList(ctx context.Context, id []int64) ([]model.Mapfix, error)

View File

@@ -32,6 +32,7 @@ func New(ctx *cli.Context) (datastore.Datastore, error) {
if ctx.Bool("migrate") {
if err := db.AutoMigrate(
&model.AuditEvent{},
&model.Map{},
&model.Mapfix{},
&model.Operation{},
&model.Submission{},

View File

@@ -13,6 +13,10 @@ func (g Gormstore) AuditEvents() datastore.AuditEvents {
return &AuditEvents{db: g.db}
}
func (g Gormstore) Maps() datastore.Maps {
return &Maps{db: g.db}
}
func (g Gormstore) Mapfixes() datastore.Mapfixes {
return &Mapfixes{db: g.db}
}

View File

@@ -0,0 +1,84 @@
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 Maps struct {
db *gorm.DB
}
func (env *Maps) Get(ctx context.Context, id int64) (model.Map, error) {
var mdl model.Map
if err := env.db.First(&mdl, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return mdl, datastore.ErrNotExist
}
return mdl, err
}
return mdl, nil
}
func (env *Maps) GetList(ctx context.Context, id []int64) ([]model.Map, error) {
var mapList []model.Map
if err := env.db.Find(&mapList, "id IN ?", id).Error; err != nil {
return mapList, err
}
return mapList, nil
}
func (env *Maps) Create(ctx context.Context, smap model.Map) (model.Map, error) {
if err := env.db.Create(&smap).Error; err != nil {
return smap, err
}
return smap, nil
}
func (env *Maps) Update(ctx context.Context, id int64, values datastore.OptionalMap) error {
if err := env.db.Model(&model.Map{}).Where("id = ?", id).Updates(values.Map()).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return datastore.ErrNotExist
}
return err
}
return nil
}
func (env *Maps) IncrementLoadCount(ctx context.Context, id int64) error {
if err := env.db.Model(&model.Map{}).Where("id = ?", id).UpdateColumn("load_count", gorm.Expr("load_count + ?", 1)).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return datastore.ErrNotExist
}
return err
}
return nil
}
func (env *Maps) Delete(ctx context.Context, id int64) error {
if err := env.db.Delete(&model.Map{}, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return datastore.ErrNotExist
}
return err
}
return nil
}
func (env *Maps) List(ctx context.Context, filters datastore.OptionalMap, page model.Page) ([]model.Map, error) {
var events []model.Map
if err := env.db.Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&events).Error; err != nil {
return nil, err
}
return events, nil
}

View File

@@ -5,51 +5,121 @@ import (
"time"
"git.itzana.me/strafesnet/go-grpc/maps"
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
"git.itzana.me/strafesnet/maps-service/pkg/model"
)
// Optional map used to update an object
type MapUpdate maps.MapRequest
type MapUpdate datastore.OptionalMap
func NewMapUpdate(id int64) *MapUpdate {
item := MapUpdate{
ID: id,
}
return &item
func NewMapUpdate() MapUpdate {
update := datastore.Optional()
return MapUpdate(update)
}
func (update *MapUpdate) SetDisplayName(display_name string) {
update.DisplayName = &display_name
func (update MapUpdate) SetDisplayName(display_name string) {
datastore.OptionalMap(update).Add("display_name", display_name)
}
func (update *MapUpdate) SetCreator(creator string) {
update.Creator = &creator
func (update MapUpdate) SetCreator(creator string) {
datastore.OptionalMap(update).Add("creator", creator)
}
func (update *MapUpdate) SetGameID(game_id int32) {
update.GameID = &game_id
func (update MapUpdate) SetGameID(game_id uint32) {
datastore.OptionalMap(update).Add("game_id", game_id)
}
func (update *MapUpdate) SetDate(date int64) {
update.Date = &date
func (update MapUpdate) SetDate(date int64) {
datastore.OptionalMap(update).Add("date", date)
}
func (update MapUpdate) SetSubmitter(submitter uint64) {
datastore.OptionalMap(update).Add("submitter", submitter)
}
func (update MapUpdate) SetThumbnail(thumbnail uint64) {
datastore.OptionalMap(update).Add("thumbnail", thumbnail)
}
func (update MapUpdate) SetAssetVersion(asset_version uint64) {
datastore.OptionalMap(update).Add("asset_version", asset_version)
}
func (update MapUpdate) SetModes(modes uint32) {
datastore.OptionalMap(update).Add("modes", modes)
}
// getters
func (update MapUpdate) GetDisplayName() (string, bool) {
value, ok := datastore.OptionalMap(update).Map()["display_name"].(string)
return value, ok
}
func (update MapUpdate) GetCreator() (string, bool) {
value, ok := datastore.OptionalMap(update).Map()["creator"].(string)
return value, ok
}
func (update MapUpdate) GetGameID() (uint32, bool) {
value, ok := datastore.OptionalMap(update).Map()["game_id"].(uint32)
return value, ok
}
func (update MapUpdate) GetDate() (int64, bool) {
value, ok := datastore.OptionalMap(update).Map()["date"].(int64)
return value, ok
}
// Optional map used to find matching objects
type MapFilter maps.MapFilter
type MapFilter datastore.OptionalMap
func NewMapFilter() *MapFilter {
item := MapFilter{}
return &item
func NewMapFilter(
) MapFilter {
filter := datastore.Optional()
return MapFilter(filter)
}
func (filter *MapFilter) SetDisplayName(display_name string) {
filter.DisplayName = &display_name
func (update MapFilter) SetDisplayName(display_name string) {
datastore.OptionalMap(update).Add("display_name", display_name)
}
func (filter *MapFilter) SetCreator(creator string) {
filter.Creator = &creator
func (update MapFilter) SetCreator(creator string) {
datastore.OptionalMap(update).Add("creator", creator)
}
func (filter *MapFilter) SetGameID(game_id int32) {
filter.GameID = &game_id
func (update MapFilter) SetGameID(game_id uint32) {
datastore.OptionalMap(update).Add("game_id", game_id)
}
func (update MapFilter) SetSubmitter(submitter uint64) {
datastore.OptionalMap(update).Add("submitter", submitter)
}
func (svc *Service) TEMP_DoMapsMigration(ctx context.Context) (error) {
// get all maps
maps, err := svc.maps.List(ctx, &maps.ListRequest{})
if err != nil {
return err
}
// create all maps
for _, item := range maps.Maps {
migrated := model.Map{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: item.GameID,
Date: time.Unix(item.Date, 0),
// CreatedAt: time.Time{},
// UpdatedAt: time.Time{},
// Submitter: 0,
// Thumbnail: 0,
// AssetVersion: 0,
// LoadCount: 0,
// Modes: 0,
}
_, err := svc.db.Maps().Create(ctx, migrated)
if err != nil {
return err
}
}
return nil
}
func (svc *Service) CreateMap(ctx context.Context, item model.Map) (int64, error) {
// 2 jobs:
// create map on maps-service
map_item, err := svc.db.Maps().Create(ctx, item)
if err != nil {
return 0, err
}
// create map on data-service
date := item.Date.Unix()
id_message, err := svc.maps.Create(ctx, &maps.MapRequest{
_, err = svc.maps.Create(ctx, &maps.MapRequest{
ID: item.ID,
DisplayName: &item.DisplayName,
Creator: &item.Creator,
@@ -60,57 +130,58 @@ func (svc *Service) CreateMap(ctx context.Context, item model.Map) (int64, error
return 0, err
}
return id_message.ID, nil
return map_item.ID, nil
}
func (svc *Service) ListMaps(ctx context.Context, filter *MapFilter, page model.Page) ([]model.Map, error) {
page_request := maps.Pagination{
Size: page.Size,
Number: page.Number,
}
request := maps.ListRequest{
Filter: (*maps.MapFilter)(filter),
Page: &page_request,
}
func (svc *Service) ListMaps(ctx context.Context, filter MapFilter, page model.Page) ([]model.Map, error) {
return svc.db.Maps().List(ctx, datastore.OptionalMap(filter), page)
}
items, err := svc.maps.List(ctx, &request)
if err != nil {
return nil, err
}
resp := make([]model.Map, len(items.Maps))
for i, item := range items.Maps {
resp[i].ID = item.ID
resp[i].DisplayName = item.DisplayName
resp[i].Creator = item.Creator
resp[i].GameID = item.GameID
resp[i].Date = time.Unix(item.Date, 0)
}
return resp, nil
func (svc *Service) GetMapList(ctx context.Context, ids []int64) ([]model.Map, error) {
return svc.db.Maps().GetList(ctx, ids)
}
func (svc *Service) DeleteMap(ctx context.Context, id int64) error {
_, err := svc.maps.Delete(ctx, &maps.IdMessage{ID: id})
return err
// Do not delete the "embedded" map, since it deletes times.
// _, err := svc.maps.Delete(ctx, &maps.IdMessage{ID: id})
return svc.db.Maps().Delete(ctx, id)
}
func (svc *Service) GetMap(ctx context.Context, id int64) (*model.Map, error) {
item, err := svc.maps.Get(ctx, &maps.IdMessage{ID: id})
func (svc *Service) GetMap(ctx context.Context, id int64) (model.Map, error) {
return svc.db.Maps().Get(ctx, id)
}
func (svc *Service) UpdateMap(ctx context.Context, id int64, pmap MapUpdate) error {
// 2 jobs:
// update map on maps-service
err := svc.db.Maps().Update(ctx, id, datastore.OptionalMap(pmap))
if err != nil {
return nil, err
return err
}
return &model.Map{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: item.GameID,
Date: time.Unix(item.Date, 0),
}, nil
// update map on data-service
update := maps.MapRequest{
ID: id,
}
if display_name, ok := pmap.GetDisplayName(); ok {
update.DisplayName = &display_name
}
if creator, ok := pmap.GetCreator(); ok {
update.Creator = &creator
}
if game_id, ok := pmap.GetGameID(); ok {
game_id_int32 := int32(game_id)
update.GameID = &game_id_int32
}
if date, ok := pmap.GetDate(); ok {
update.Date = &date
}
_, err = svc.maps.Update(ctx, &update)
if err != nil {
return err
}
return nil
}
func (svc *Service) UpdateMap(ctx context.Context, pmap *MapUpdate) error {
_, err := svc.maps.Update(ctx, (*maps.MapRequest)(pmap))
return err
func (svc *Service) IncrementMapLoadCount(ctx context.Context, id int64) error {
return svc.db.Maps().IncrementLoadCount(ctx, id)
}

View File

@@ -9,6 +9,29 @@ import (
"git.itzana.me/strafesnet/maps-service/pkg/service"
)
// MigrateMaps implements migrateMaps operation.
//
// Perform maps migration.
//
// POST /migrate
func (svc *Service) MigrateMaps(ctx context.Context) (error) {
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return ErrUserInfo
}
has_role, err := userInfo.HasRoleSubmissionRelease()
if err != nil {
return err
}
if !has_role {
return ErrPermissionDeniedNeedRoleSubmissionRelease
}
return svc.inner.TEMP_DoMapsMigration(ctx)
}
// ListMaps implements listMaps operation.
//
// Get list of maps.
@@ -24,7 +47,7 @@ func (svc *Service) ListMaps(ctx context.Context, params api.ListMapsParams) ([]
filter.SetCreator(creator)
}
if game_id, game_id_ok := params.GameID.Get(); game_id_ok{
filter.SetGameID(game_id)
filter.SetGameID(uint32(game_id))
}
items, err := svc.inner.ListMaps(ctx,

View File

@@ -1079,11 +1079,16 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas
var GameID = int32(submission.GameID)
// create each map with go-grpc
_, err := svc.inner.CreateMap(ctx, model.Map{
ID: int64(submission.UploadedAssetID),
DisplayName: submission.DisplayName,
Creator: submission.Creator,
GameID: GameID,
Date: time.Unix(date, 0),
ID: int64(submission.UploadedAssetID),
DisplayName: submission.DisplayName,
Creator: submission.Creator,
GameID: GameID,
Date: time.Unix(date, 0),
Submitter: int64(submission.Submitter),
// Thumbnail: 0,
// AssetVersion: 0,
// LoadCount: 0,
// Modes: 0,
})
if err != nil {
return err