diff --git a/.gitignore b/.gitignore index 65a20b6..d9601cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/* *.iml *.yaml +clouddb +clouddb-cli/clouddb-cli diff --git a/app.yaml.in b/app.yaml.in index d364240..c18bbb2 100644 --- a/app.yaml.in +++ b/app.yaml.in @@ -1,4 +1,5 @@ -runtime: go111 +runtime: go125 +app_engine_apis: true env_variables: Basic_Auth: '< the Basic_Auth Secret - in sync with GC_CLOUD_DB_BASIC_AUTH in GC config.pri >' diff --git a/clouddb-cli/client.go b/clouddb-cli/client.go new file mode 100644 index 0000000..a80f6a7 --- /dev/null +++ b/clouddb-cli/client.go @@ -0,0 +1,224 @@ +/* + * Copyright 2025 Magnus Gille + */ + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" +) + +type CloudClient struct { + BaseURL string + Secret string + Client *http.Client +} + +func NewCloudClient(host, secret string) *CloudClient { + return &CloudClient{ + BaseURL: fmt.Sprintf("http://%s/v1", host), + Secret: secret, + Client: &http.Client{}, + } +} + +func (c *CloudClient) doRequest(method, endpoint string, body interface{}, result interface{}, params map[string]string) error { + var bodyReader *bytes.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("failed to marshal body: %w", err) + } + bodyReader = bytes.NewReader(jsonBody) + } else { + bodyReader = bytes.NewReader([]byte{}) + } + + u, err := url.Parse(c.BaseURL + endpoint) + if err != nil { + return fmt.Errorf("failed to parse url: %w", err) + } + + if params != nil { + q := u.Query() + for k, v := range params { + q.Set(k, v) + } + u.RawQuery = q.Encode() + } + + req, err := http.NewRequest(method, u.String(), bodyReader) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + if c.Secret != "" { + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", c.Secret)) + } + + resp, err := c.Client.Do(req) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + respBody, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody)) + } + + if result != nil && resp.StatusCode != http.StatusNoContent { + if err := json.NewDecoder(resp.Body).Decode(result); err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + } + + return nil +} + +// GChart Methods + +func (c *CloudClient) CreateGChart(chart GChartPostAPIv1) (string, error) { + // The API returns the ID as a string in the body, not JSON + // So we need to handle this specific case slightly differently or just read body + + jsonBody, err := json.Marshal(chart) + if err != nil { + return "", err + } + + req, err := http.NewRequest("POST", c.BaseURL+"/gchart/", bytes.NewReader(jsonBody)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + if c.Secret != "" { + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", c.Secret)) + } + + resp, err := c.Client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode >= 400 { + return "", fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBytes)) + } + + return string(respBytes), nil +} + +func (c *CloudClient) GetGChart(id string) (*GChartGetAPIv1, error) { + var chart GChartGetAPIv1 + err := c.doRequest("GET", "/gchart/"+id, nil, &chart, nil) + return &chart, err +} + +func (c *CloudClient) SearchGChartHeaders(dateFrom string) (GChartAPIv1HeaderOnlyList, error) { + var headers GChartAPIv1HeaderOnlyList + params := map[string]string{} + if dateFrom != "" { + params["dateFrom"] = dateFrom + } + err := c.doRequest("GET", "/gchartheader", nil, &headers, params) + return headers, err +} + +// Version Methods + +func (c *CloudClient) CreateVersion(ver VersionEntityPostAPIv1) (string, error) { + // Similar to GChart, returns ID as string/int + // doRequest expects JSON response for result pointers usually, but create returns simple string ID sometimes? + // Checking code: response.WriteHeaderAndEntity(http.StatusCreated, strconv.FormatInt(key.IntID(), 10)) + // This writes the ID as the body. + + // So we use a custom request here as well for simplicity + jsonBody, err := json.Marshal(ver) + if err != nil { + return "", err + } + + req, err := http.NewRequest("POST", c.BaseURL+"/version", bytes.NewReader(jsonBody)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + if c.Secret != "" { + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", c.Secret)) + } + + resp, err := c.Client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode >= 400 { + return "", fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBytes)) + } + + return string(respBytes), nil +} + +func (c *CloudClient) GetVersions(version string) (VersionEntityGetAPIv1List, error) { + var versions VersionEntityGetAPIv1List + params := map[string]string{} + if version != "" { + params["version"] = version + } + err := c.doRequest("GET", "/version", nil, &versions, params) + return versions, err +} + +func (c *CloudClient) GetLatestVersion() (*VersionEntityGetAPIv1, error) { + var ver VersionEntityGetAPIv1 + err := c.doRequest("GET", "/version/latest", nil, &ver, nil) + return &ver, err +} + +// Telemetry Methods + +func (c *CloudClient) UpsertTelemetry(tel TelemetryEntityPostAPIv1) (*TelemetryEntityGetAPIv1, error) { + var result TelemetryEntityGetAPIv1 + // PUT /telemetry returns the object + err := c.doRequest("PUT", "/telemetry", tel, &result, nil) + return &result, err +} + +func (c *CloudClient) GetTelemetry(createdAfter, updatedAfter, os, version string) (TelemetryEntityGetAPIv1List, error) { + var list TelemetryEntityGetAPIv1List + params := map[string]string{} + if createdAfter != "" { + params["createdAfter"] = createdAfter + } + if updatedAfter != "" { + params["updatedAfter"] = updatedAfter + } + if os != "" { + params["os"] = os + } + if version != "" { + params["version"] = version + } + + err := c.doRequest("GET", "/telemetry", nil, &list, params) + return list, err +} diff --git a/clouddb-cli/main.go b/clouddb-cli/main.go new file mode 100644 index 0000000..21e5370 --- /dev/null +++ b/clouddb-cli/main.go @@ -0,0 +1,272 @@ +/* + * Copyright 2025 Magnus Gille + */ + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "time" +) + +func main() { + hostPtr := flag.String("host", "localhost:8080", "CloudDB host (e.g. localhost:8080)") + secretPtr := flag.String("secret", "", "Basic Auth Secret") + + // We need to parse flags before subcommands to get host/secret + // However, standard flag package stops at the first non-flag argument (the subcommand). + // So we can just parse commonly. + flag.Parse() + + if len(flag.Args()) < 1 { + printUsage() + os.Exit(1) + } + + client := NewCloudClient(*hostPtr, *secretPtr) + subcommand := flag.Arg(0) + + switch subcommand { + case "gchart": + handleGChart(client, flag.Args()[1:]) + case "version": + handleVersion(client, flag.Args()[1:]) + case "telemetry": + handleTelemetry(client, flag.Args()[1:]) + default: + fmt.Printf("Unknown subcommand: %s\n", subcommand) + printUsage() + os.Exit(1) + } +} + +func printUsage() { + fmt.Println("Usage: cloud-cli [flags] [args]") + fmt.Println("Flags:") + flag.PrintDefaults() + fmt.Println("\nSubcommands:") + fmt.Println(" gchart [args]") + fmt.Println(" version [args]") + fmt.Println(" telemetry [args]") +} + +func handleGChart(client *CloudClient, args []string) { + if len(args) < 1 { + fmt.Println("Usage: gchart ...") + return + } + + cmd := args[0] + switch cmd { + case "get": + if len(args) < 2 { + fmt.Println("Usage: gchart get ") + return + } + id := args[1] + chart, err := client.GetGChart(id) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + printJSON(chart) + + case "search": + // Usage: gchart search [flags] + // Flags: -date -curated + + searchCmd := flag.NewFlagSet("search", flag.ExitOnError) + dateFromPtr := searchCmd.String("date", "", "Date from (RFC3339)") + curatedPtr := searchCmd.Bool("curated", false, "Filter for curated charts only") + + searchCmd.Parse(args[1:]) + + headers, err := client.SearchGChartHeaders(*dateFromPtr) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + if *curatedPtr { + var filtered GChartAPIv1HeaderOnlyList + for _, h := range headers { + if h.Header.Curated { + filtered = append(filtered, h) + } + } + headers = filtered + } + + printJSON(headers) + + case "create": + // Usage: gchart create + if len(args) < 2 { + fmt.Println("Usage: gchart create ") + return + } + data, err := os.ReadFile(args[1]) + if err != nil { + fmt.Printf("Error reading file: %v\n", err) + return + } + var chart GChartPostAPIv1 + if err := json.Unmarshal(data, &chart); err != nil { + fmt.Printf("Error parsing JSON: %v\n", err) + return + } + id, err := client.CreateGChart(chart) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("Created GChart with ID: %s\n", id) + + default: + fmt.Printf("Unknown gchart command: %s\n", cmd) + } +} + +func handleVersion(client *CloudClient, args []string) { + if len(args) < 1 { + fmt.Println("Usage: version ...") + return + } + + cmd := args[0] + switch cmd { + case "get": + // Usage: version get [minVersion] + minVer := "" + if len(args) > 1 { + minVer = args[1] + } + vers, err := client.GetVersions(minVer) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + printJSON(vers) + + case "latest": + ver, err := client.GetLatestVersion() + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + printJSON(ver) + + case "create": + // Usage: version create + if len(args) < 2 { + fmt.Println("Usage: version create ") + return + } + data, err := os.ReadFile(args[1]) + if err != nil { + fmt.Printf("Error reading file: %v\n", err) + return + } + var ver VersionEntityPostAPIv1 + if err := json.Unmarshal(data, &ver); err != nil { + fmt.Printf("Error parsing JSON: %v\n", err) + return + } + id, err := client.CreateVersion(ver) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("Created Version with ID: %s\n", id) + + default: + fmt.Printf("Unknown version command: %s\n", cmd) + } +} + +func handleTelemetry(client *CloudClient, args []string) { + if len(args) < 1 { + fmt.Println("Usage: telemetry ...") + return + } + + cmd := args[0] + switch cmd { + case "list": + // Usage: telemetry list [createdAfter] [updatedAfter] [os] [version] + // This argument parsing is simple/brittle for simplicity + // Let's just say we support positional args or just one filter + // Or assume the user passes specific flags? But we are already deep in subcommands. + // Let's implement simple positional: list + // Pass "_" to skip. + + createdAfter, updatedAfter, osVal, verVal := "", "", "", "" + if len(args) > 1 && args[1] != "_" { + createdAfter = args[1] + } + if len(args) > 2 && args[2] != "_" { + updatedAfter = args[2] + } + if len(args) > 3 && args[3] != "_" { + osVal = args[3] + } + if len(args) > 4 && args[4] != "_" { + verVal = args[4] + } + + list, err := client.GetTelemetry(createdAfter, updatedAfter, osVal, verVal) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + printJSON(list) + + case "upsert": + // Usage: telemetry upsert [os] [version] [increment] + if len(args) < 2 { + fmt.Println("Usage: telemetry upsert [os] [version] [increment=1]") + return + } + key := args[1] + osVal := "Linux" + if len(args) > 2 { + osVal = args[2] + } + verVal := "3.6" + if len(args) > 3 { + verVal = args[3] + } + inc := int64(1) + // skip parsing increment complexity for now or... + + tel := TelemetryEntityPostAPIv1{ + UserKey: key, + OS: osVal, + GCVersion: verVal, + Increment: inc, + LastChange: time.Now().Format(time.RFC3339), + } + + res, err := client.UpsertTelemetry(tel) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + printJSON(res) + + default: + fmt.Printf("Unknown telemetry command: %s\n", cmd) + } +} + +func printJSON(v interface{}) { + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + return + } + fmt.Println(string(b)) +} diff --git a/clouddb-cli/types.go b/clouddb-cli/types.go new file mode 100644 index 0000000..18a8ca0 --- /dev/null +++ b/clouddb-cli/types.go @@ -0,0 +1,97 @@ +/* + * Copyright 2025 Magnus Gille + */ + +package main + +// Common Structures +type CommonAPIHeaderV1 struct { + Id int64 `json:"id"` + Key string `json:"key"` + Name string `json:"name"` + Description string `json:"description"` + GcVersion string `json:"gcversion"` + LastChanged string `json:"lastChange"` + CreatorId string `json:"creatorId"` + Language string `json:"language"` + Curated bool `json:"curated"` + Deleted bool `json:"deleted"` +} + +// GChart Structures +type GChartPostAPIv1 struct { + Header CommonAPIHeaderV1 `json:"header"` + ChartSport string `json:"chartSport"` + ChartType string `json:"chartType"` + ChartView string `json:"chartView"` + ChartDef string `json:"chartDef"` + Image string `json:"image"` + CreatorNick string `json:"creatorNick"` + CreatorEmail string `json:"creatorEmail"` +} + +type GChartGetAPIv1 struct { + Header CommonAPIHeaderV1 `json:"header"` + ChartSport string `json:"chartSport"` + ChartType string `json:"chartType"` + ChartView string `json:"chartView"` + ChartDef string `json:"chartDef"` + Image string `json:"image"` + CreatorNick string `json:"creatorNick"` + CreatorEmail string `json:"creatorEmail"` + DLCounter int `json:"downloadCount"` +} + +type GChartGetAPIv1List []GChartGetAPIv1 + +type GChartAPIv1HeaderOnly struct { + Header CommonAPIHeaderV1 `json:"header"` + ChartSport string `json:"chartSport"` + ChartType string `json:"chartType"` + ChartView string `json:"chartView"` +} +type GChartAPIv1HeaderOnlyList []GChartAPIv1HeaderOnly + +// Version Structures +type VersionEntityPostAPIv1 struct { + Version int `json:"version"` + Type int `json:"releaseType"` + URL string `json:"downloadURL"` + VersionText string `json:"versionText"` + Text string `json:"text"` +} + +type VersionEntityGetAPIv1 struct { + Id int64 `json:"id"` + Version int `json:"version"` + Type int `json:"releaseType"` + URL string `json:"downloadURL"` + VersionText string `json:"versionText"` + Text string `json:"text"` +} + +type VersionEntityGetAPIv1List []VersionEntityGetAPIv1 + +// Telemetry Structures +type TelemetryEntityPostAPIv1 struct { + UserKey string `json:"key"` + LastChange string `json:"lastChange"` + OS string `json:"operatingSystem"` + GCVersion string `json:"version"` + Increment int64 `json:"increment"` +} + +type TelemetryEntityGetAPIv1 struct { + UserKey string `json:"key"` + Country string `json:"country"` + Region string `json:"region"` + City string `json:"city"` + CityLatLong string `json:"cityLatLong"` + CreateDate string `json:"createDate"` + LastChange string `json:"lastChange"` + UseCount int64 `json:"useCount"` + OS string `json:"operatingSystem"` + GCVersion string `json:"version"` +} + +type TelemetryEntityGetAPIv1List []TelemetryEntityGetAPIv1 diff --git a/entity_curator.go b/entity_curator.go index 2cfc069..72bcb37 100644 --- a/entity_curator.go +++ b/entity_curator.go @@ -15,30 +15,27 @@ * along with this program. If not, see . */ - package main import ( "net/http" "strconv" - "golang.org/x/net/context" + "context" + "google.golang.org/appengine" "google.golang.org/appengine/datastore" "github.com/emicklei/go-restful" - ) - // ---------------------------------------------------------------------------------------------------------------// // Golden Cheetah curator (curatorentity) which is stored in DB // ---------------------------------------------------------------------------------------------------------------// type CuratorEntity struct { - CuratorId string - Nickname string - Email string - + CuratorId string + Nickname string + Email string } // ---------------------------------------------------------------------------------------------------------------// @@ -47,15 +44,14 @@ type CuratorEntity struct { // Full structure for GET and PUT type CuratorAPIv1 struct { - Id int64 `json:"id"` - CuratorId string `json:"curatorId"` - Nickname string `json:"nickname"` - Email string `json:"email"` + Id int64 `json:"id"` + CuratorId string `json:"curatorId"` + Nickname string `json:"nickname"` + Email string `json:"email"` } type CuratorAPIv1List []CuratorAPIv1 - // ---------------------------------------------------------------------------------------------------------------// // Data Storage View // ---------------------------------------------------------------------------------------------------------------// @@ -63,26 +59,23 @@ type CuratorAPIv1List []CuratorAPIv1 const curatorDBEntity = "curatorentity" const curatorDBEntityRootKey = "curatorroot" - func mapAPItoDBCurator(api *CuratorAPIv1, db *CuratorEntity) { db.CuratorId = api.CuratorId db.Nickname = api.Nickname db.Email = api.Email } - -func mapDBtoAPICurator(db *CuratorEntity, api *CuratorAPIv1 ) { +func mapDBtoAPICurator(db *CuratorEntity, api *CuratorAPIv1) { api.CuratorId = db.CuratorId api.Nickname = db.Nickname api.Email = db.Email } - // supporting functions // curatorEntityKey returns the key used for all curatorEntity entries. func curatorEntityRootKey(c context.Context) *datastore.Key { - return datastore.NewKey(c, curatorDBEntity, curatorDBEntityRootKey, 0, nil) + return DB.NewKey(c, curatorDBEntity, curatorDBEntityRootKey, 0, nil) } // ---------------------------------------------------------------------------------------------------------------// @@ -94,7 +87,7 @@ func insertCurator(request *restful.Request, response *restful.Response) { curator := new(CuratorAPIv1) if err := request.ReadEntity(curator); err != nil { - addPlainTextError(response,http.StatusInternalServerError, err.Error()) + addPlainTextError(response, http.StatusInternalServerError, err.Error()) return } @@ -105,9 +98,9 @@ func insertCurator(request *restful.Request, response *restful.Response) { mapAPItoDBCurator(curator, curatorDB) // and now store it - key := datastore.NewIncompleteKey(ctx, curatorDBEntity, curatorEntityRootKey(ctx)) - key, err := datastore.Put(ctx, key, curatorDB); - if err != nil { + key := DB.NewIncompleteKey(ctx, curatorDBEntity, curatorEntityRootKey(ctx)) + key, err := DB.Put(ctx, key, curatorDB) + if err != nil { if appengine.IsOverQuota(err) { // return 503 and a text similar to what GAE is returning as well addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota") @@ -122,17 +115,16 @@ func insertCurator(request *restful.Request, response *restful.Response) { } - func getCurator(request *restful.Request, response *restful.Response) { ctx := appengine.NewContext(request.Request) - curatorString := request.QueryParameter("curatorId"); + curatorString := request.QueryParameter("curatorId") - var q *datastore.Query + var q Query if curatorString != "" { - q = datastore.NewQuery(curatorDBEntity).Filter("CuratorId =", curatorString) + q = DB.NewQuery(curatorDBEntity).Filter("CuratorId =", curatorString) } else { - q = datastore.NewQuery(curatorDBEntity) + q = DB.NewQuery(curatorDBEntity) } var curatorOnDBList []CuratorEntity @@ -153,9 +145,8 @@ func getCurator(request *restful.Request, response *restful.Response) { var curator CuratorAPIv1 mapDBtoAPICurator(&curatorDB, &curator) curator.Id = k[i].IntID() - curatorList = append (curatorList, curator) + curatorList = append(curatorList, curator) } response.WriteEntity(curatorList) } - diff --git a/entity_gchart.go b/entity_gchart.go index 486d708..b164bb1 100644 --- a/entity_gchart.go +++ b/entity_gchart.go @@ -15,7 +15,6 @@ * along with this program. If not, see . */ - package main import ( @@ -25,7 +24,8 @@ import ( "strconv" "time" - "golang.org/x/net/context" + "context" + "google.golang.org/appengine" "google.golang.org/appengine/datastore" "google.golang.org/appengine/log" @@ -35,24 +35,23 @@ import ( "github.com/emicklei/go-restful" ) - // ---------------------------------------------------------------------------------------------------------------// // Full Golden Cheetah chart definition (gchartentity) which is stored in DB // ---------------------------------------------------------------------------------------------------------------// type GChartEntity struct { Header CommonEntityHeader - ChartSport string `datastore:",noindex"` - ChartType string `datastore:",noindex"` - ChartView string `datastore:",noindex"` - ChartDef string `datastore:",noindex"` - Image []byte `datastore:",noindex"` - CreatorNick string `datastore:",noindex"` - CreatorEmail string `datastore:",noindex"` + ChartSport string `datastore:",noindex"` + ChartType string `datastore:",noindex"` + ChartView string `datastore:",noindex"` + ChartDef string `datastore:",noindex"` + Image []byte `datastore:",noindex"` + CreatorNick string `datastore:",noindex"` + CreatorEmail string `datastore:",noindex"` Internal GChartEntityInternal } type GChartEntityHeaderOnly struct { - Header CommonEntityHeader + Header CommonEntityHeader ChartSport string ChartType string ChartView string @@ -60,10 +59,9 @@ type GChartEntityHeaderOnly struct { // Internal attributes which must not be filled by POST or PUT (but are returned on GET) type GChartEntityInternal struct { - DLCounter int `datastore:",noindex"` + DLCounter int `datastore:",noindex"` } - // ---------------------------------------------------------------------------------------------------------------// // API View Definition // ---------------------------------------------------------------------------------------------------------------// @@ -71,42 +69,39 @@ type GChartEntityInternal struct { // Full structure for GET type GChartGetAPIv1 struct { Header CommonAPIHeaderV1 `json:"header"` - ChartSport string `json:"chartSport"` - ChartType string `json:"chartType"` - ChartView string `json:"chartView"` - ChartDef string `json:"chartDef"` - Image string `json:"image"` - CreatorNick string `json:"creatorNick"` - CreatorEmail string `json:"creatorEmail"` - DLCounter int `json:"downloadCount"` + ChartSport string `json:"chartSport"` + ChartType string `json:"chartType"` + ChartView string `json:"chartView"` + ChartDef string `json:"chartDef"` + Image string `json:"image"` + CreatorNick string `json:"creatorNick"` + CreatorEmail string `json:"creatorEmail"` + DLCounter int `json:"downloadCount"` } // Reduced structure for POST and PUT (without internal fields) type GChartPostAPIv1 struct { Header CommonAPIHeaderV1 `json:"header"` - ChartSport string `json:"chartSport"` - ChartType string `json:"chartType"` - ChartView string `json:"chartView"` - ChartDef string `json:"chartDef"` - Image string `json:"image"` - CreatorNick string `json:"creatorNick"` - CreatorEmail string `json:"creatorEmail"` + ChartSport string `json:"chartSport"` + ChartType string `json:"chartType"` + ChartView string `json:"chartView"` + ChartDef string `json:"chartDef"` + Image string `json:"image"` + CreatorNick string `json:"creatorNick"` + CreatorEmail string `json:"creatorEmail"` } - type GChartGetAPIv1List []GChartGetAPIv1 // Header only structure type GChartAPIv1HeaderOnly struct { - Header CommonAPIHeaderV1 `json:"header"` - ChartSport string `json:"chartSport"` - ChartType string `json:"chartType"` - ChartView string `json:"chartView"` + Header CommonAPIHeaderV1 `json:"header"` + ChartSport string `json:"chartSport"` + ChartType string `json:"chartType"` + ChartView string `json:"chartView"` } type GChartAPIv1HeaderOnlyList []GChartAPIv1HeaderOnly - - // ---------------------------------------------------------------------------------------------------------------// // Data Storage View // ---------------------------------------------------------------------------------------------------------------// @@ -135,7 +130,6 @@ func mapAPItoDBGChart(api *GChartPostAPIv1, db *GChartEntity) { db.CreatorEmail = api.CreatorEmail } - func mapDBtoAPIGChart(db *GChartEntity, api *GChartGetAPIv1) { mapDBtoAPICommonHeader(&db.Header, &api.Header) api.ChartSport = db.ChartSport @@ -148,13 +142,11 @@ func mapDBtoAPIGChart(db *GChartEntity, api *GChartGetAPIv1) { api.DLCounter = db.Internal.DLCounter } - - // supporting functions // chartEntityKey returns the key used for all chartEntity entries. func gchartEntityRootKey(ctx context.Context) *datastore.Key { - return datastore.NewKey(ctx, gChartDBEntity, gChartDBEntityRootKey, 0, nil) + return DB.NewKey(ctx, gChartDBEntity, gChartDBEntityRootKey, 0, nil) } // ---------------------------------------------------------------------------------------------------------------// @@ -162,7 +154,7 @@ func gchartEntityRootKey(ctx context.Context) *datastore.Key { // ---------------------------------------------------------------------------------------------------------------// func insertGChart(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() chart := new(GChartPostAPIv1) if err := request.ReadEntity(chart); err != nil { @@ -183,7 +175,7 @@ func insertGChart(request *restful.Request, response *restful.Response) { chartDB.Internal.DLCounter = 0 // auto-curate if a registered "curator" is adding a gchart - curatorQuery := datastore.NewQuery(curatorDBEntity).Filter("CuratorId =", chartDB.Header.CreatorId) + curatorQuery := DB.NewQuery(curatorDBEntity).Filter("CuratorId =", chartDB.Header.CreatorId) counter, _ := curatorQuery.Count(ctx) // ignore errors/just leave uncurated if counter == 1 { chartDB.Header.Curated = true @@ -192,10 +184,10 @@ func insertGChart(request *restful.Request, response *restful.Response) { } // and now store it - key := datastore.NewIncompleteKey(ctx, gChartDBEntity, gchartEntityRootKey(ctx)) - key, err := datastore.Put(ctx, key, chartDB); + key := DB.NewIncompleteKey(ctx, gChartDBEntity, gchartEntityRootKey(ctx)) + key, err := DB.Put(ctx, key, chartDB) if err != nil { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } @@ -205,7 +197,7 @@ func insertGChart(request *restful.Request, response *restful.Response) { } func updateGChart(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() chart := new(GChartPostAPIv1) if err := request.ReadEntity(chart); err != nil { @@ -218,11 +210,11 @@ func updateGChart(request *restful.Request, response *restful.Response) { return } - key := datastore.NewKey(ctx, gChartDBEntity, "", chart.Header.Id, gchartEntityRootKey(ctx)) + key := DB.NewKey(ctx, gChartDBEntity, "", chart.Header.Id, gchartEntityRootKey(ctx)) // get the current chart to retrieve the current DL counter currentChartDB := new(GChartEntity) - err := datastore.Get(ctx, key, currentChartDB) + err := DB.Get(ctx, key, currentChartDB) if err != nil && !isErrFieldMismatch(err) { commonResponseErrorProcessing(response, err) return @@ -238,8 +230,8 @@ func updateGChart(request *restful.Request, response *restful.Response) { // and now store it - if _, err := datastore.Put(ctx, key, chartDB); err != nil { - commonResponseErrorProcessing (response, err) + if _, err := DB.Put(ctx, key, chartDB); err != nil { + commonResponseErrorProcessing(response, err) return } @@ -248,7 +240,7 @@ func updateGChart(request *restful.Request, response *restful.Response) { } func getGChartHeader(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() var date time.Time var err error @@ -263,16 +255,16 @@ func getGChartHeader(request *restful.Request, response *restful.Response) { date = time.Time{} } - const maxNumberOfHeadersPerCall = 200; // this has to be equal to GoldenCheetah - CloudDBChartClient class + const maxNumberOfHeadersPerCall = 200 // this has to be equal to GoldenCheetah - CloudDBChartClient class - q := datastore.NewQuery(gChartDBEntity).Filter("Header.LastChanged >=", date).Order("Header.LastChanged").Limit(maxNumberOfHeadersPerCall) + q := DB.NewQuery(gChartDBEntity).Filter("Header.LastChanged >=", date).Order("Header.LastChanged").Limit(maxNumberOfHeadersPerCall) var chartHeaderList GChartAPIv1HeaderOnlyList var chartsOnDBList []GChartEntityHeaderOnly k, err := q.GetAll(ctx, &chartsOnDBList) if err != nil && !isErrFieldMismatch(err) { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } @@ -288,14 +280,14 @@ func getGChartHeader(request *restful.Request, response *restful.Response) { } // write Info Log - log.Infof(ctx, "GetHeader from: %s", dateString ) + log.Infof(ctx, "GetHeader from: %s", dateString) response.WriteHeaderAndEntity(http.StatusOK, chartHeaderList) } func getGChartHeaderCount(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() var date time.Time var err error @@ -309,7 +301,7 @@ func getGChartHeaderCount(request *restful.Request, response *restful.Response) date = time.Time{} } - q := datastore.NewQuery(gChartDBEntity).Filter("Header.LastChanged >=", date).Order("-Header.LastChanged") + q := DB.NewQuery(gChartDBEntity).Filter("Header.LastChanged >=", date).Order("-Header.LastChanged") counter, _ := q.Count(ctx) response.WriteHeaderAndEntity(http.StatusOK, counter) @@ -317,21 +309,21 @@ func getGChartHeaderCount(request *restful.Request, response *restful.Response) } func getGChartById(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() id := request.PathParameter("id") i, err := strconv.ParseInt(id, 10, 64) if err != nil { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } - key := datastore.NewKey(ctx, gChartDBEntity, "", i, gchartEntityRootKey(ctx)) + key := DB.NewKey(ctx, gChartDBEntity, "", i, gchartEntityRootKey(ctx)) chartDB := new(GChartEntity) - err = datastore.Get(ctx, key, chartDB) + err = DB.Get(ctx, key, chartDB) if err != nil && !isErrFieldMismatch(err) { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } @@ -351,27 +343,27 @@ func deleteGChartById(request *restful.Request, response *restful.Response) { func incrementGChartUsageById(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() id := request.PathParameter("id") i, err := strconv.ParseInt(id, 10, 64) if err != nil { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } - key := datastore.NewKey(ctx, gChartDBEntity, "", i, gchartEntityRootKey(ctx)) + key := DB.NewKey(ctx, gChartDBEntity, "", i, gchartEntityRootKey(ctx)) chartDB := new(GChartEntity) - err = datastore.Get(ctx, key, chartDB) + err = DB.Get(ctx, key, chartDB) if err != nil && !isErrFieldMismatch(err) { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } // update the download counter but ignore any errors on writing chartDB.Internal.DLCounter += 1 - datastore.Put(ctx, key, chartDB) + DB.Put(ctx, key, chartDB) response.WriteHeaderAndEntity(http.StatusNoContent, "") @@ -382,7 +374,7 @@ func curateGChartById(request *restful.Request, response *restful.Response) { newStatusString := request.QueryParameter("newStatus") b, err := strconv.ParseBool(newStatusString) if err != nil { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } changeGChartById(request, response, false, true, b) @@ -392,7 +384,7 @@ func curateGChartById(request *restful.Request, response *restful.Response) { // ------------------- supporting functions ------------------------------------------------ func changeGChartById(request *restful.Request, response *restful.Response, changeDeleted bool, changeCurated bool, newStatus bool) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() id := request.PathParameter("id") i, err := strconv.ParseInt(id, 10, 64) @@ -401,12 +393,12 @@ func changeGChartById(request *restful.Request, response *restful.Response, chan return } - key := datastore.NewKey(ctx, gChartDBEntity, "", i, gchartEntityRootKey(ctx)) + key := DB.NewKey(ctx, gChartDBEntity, "", i, gchartEntityRootKey(ctx)) chartDB := new(GChartEntity) - err = datastore.Get(ctx, key, chartDB) + err = DB.Get(ctx, key, chartDB) if err != nil && !isErrFieldMismatch(err) { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } @@ -428,7 +420,7 @@ func changeGChartById(request *restful.Request, response *restful.Response, chan chartDB.Header.LastChanged = time.Now() } - if _, err := datastore.Put(ctx, key, chartDB); err != nil { + if _, err := DB.Put(ctx, key, chartDB); err != nil { if appengine.IsOverQuota(err) { // return 503 and a text similar to what GAE is returning as well addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota") @@ -442,4 +434,3 @@ func changeGChartById(request *restful.Request, response *restful.Response, chan response.WriteHeaderAndEntity(http.StatusNoContent, "") } - diff --git a/entity_status.go b/entity_status.go index c1b16dd..b77dd81 100644 --- a/entity_status.go +++ b/entity_status.go @@ -15,7 +15,6 @@ * along with this program. If not, see . */ - package main import ( @@ -24,15 +23,14 @@ import ( "strconv" "time" - "golang.org/x/net/context" + "context" + "google.golang.org/appengine" "google.golang.org/appengine/datastore" - "google.golang.org/appengine/memcache" "github.com/emicklei/go-restful" ) - // ---------------------------------------------------------------------------------------------------------------// // Golden Cheetah curator (statusentity) which is stored in DB // ---------------------------------------------------------------------------------------------------------------// @@ -43,15 +41,13 @@ type StatusEntity struct { // Constants defined for documentation purposes - as they are set by GC const ( - Status_Ok = 10 + Status_Ok = 10 Status_PartialFailure = 20 - Status_Outage = 30 + Status_Outage = 30 ) - - type StatusEntityText struct { - Text string `datastore:",noindex"` + Text string `datastore:",noindex"` } // ---------------------------------------------------------------------------------------------------------------// @@ -60,21 +56,21 @@ type StatusEntityText struct { // Full structure for POST/PUT type StatusEntityPostAPIv1 struct { - Id int64 `json:"id"` - Status int `json:"status"` - ChangeDate string `json:"changeDate"` - Text string `json:"text"` + Id int64 `json:"id"` + Status int `json:"status"` + ChangeDate string `json:"changeDate"` + Text string `json:"text"` } type StatusEntityGetAPIv1 struct { - Id int64 `json:"id"` - Status int `json:"status"` - ChangeDate string `json:"changeDate"` + Id int64 `json:"id"` + Status int `json:"status"` + ChangeDate string `json:"changeDate"` } type StatusEntityGetTextAPIv1 struct { - Id int64 `json:"id"` - Text string `json:"text"` + Id int64 `json:"id"` + Text string `json:"text"` } type StatusEntityGetAPIv1List []StatusEntityGetAPIv1 @@ -108,12 +104,11 @@ func mapDBtoAPIStatus(db *StatusEntity, api *StatusEntityGetAPIv1) { api.ChangeDate = db.ChangeDate.Format(dateTimeLayout) } - // supporting functions // statusEntityKey returns the key used for all statusEntity entries. func statusEntityRootKey(ctx context.Context) *datastore.Key { - return datastore.NewKey(ctx, statusDBEntity, statusDBEntityRootKey, 0, nil) + return DB.NewKey(ctx, statusDBEntity, statusDBEntityRootKey, 0, nil) } // ---------------------------------------------------------------------------------------------------------------// @@ -136,8 +131,8 @@ func insertStatus(request *restful.Request, response *restful.Response) { mapAPItoDBStatus(status, statusDB) // and now store it - key := datastore.NewIncompleteKey(ctx, statusDBEntity, statusEntityRootKey(ctx)) - key, err := datastore.Put(ctx, key, statusDB); + key := DB.NewIncompleteKey(ctx, statusDBEntity, statusEntityRootKey(ctx)) + key, err := DB.Put(ctx, key, statusDB) if err != nil { if appengine.IsOverQuota(err) { // return 503 and a text similar to what GAE is returning as well @@ -152,8 +147,8 @@ func insertStatus(request *restful.Request, response *restful.Response) { statusDBText := new(StatusEntityText) statusDBText.Text = status.Text // and now store it as child of statusEntry - key := datastore.NewIncompleteKey(ctx, statusDBEntityText, key) - key, err := datastore.Put(ctx, key, statusDBText); + key := DB.NewIncompleteKey(ctx, statusDBEntityText, key) + key, err := DB.Put(ctx, key, statusDBText) if err != nil { if appengine.IsOverQuota(err) { // return 503 and a text similar to what GAE is returning as well @@ -171,7 +166,7 @@ func insertStatus(request *restful.Request, response *restful.Response) { in.ChangeDate = status.ChangeDate // flush the memcache - memcache.Flush(ctx) + DB.CacheFlush(ctx) // send back the key response.WriteHeaderAndEntity(http.StatusCreated, strconv.FormatInt(key.IntID(), 10)) @@ -193,7 +188,7 @@ func getStatus(request *restful.Request, response *restful.Response) { date = time.Time{} } - q := datastore.NewQuery(statusDBEntity).Filter("ChangeDate >=", date).Order("-ChangeDate") + q := DB.NewQuery(statusDBEntity).Filter("ChangeDate >=", date).Order("-ChangeDate") var statusList StatusEntityGetAPIv1List @@ -226,12 +221,12 @@ func getCurrentStatus(request *restful.Request, response *restful.Response) { var statusAPI StatusEntityGetAPIv1 // first check Memcache - if _, err := memcache.Gob.Get(ctx, statusMemcacheKey, &statusAPI); err == nil { + if err := DB.CacheGet(ctx, statusMemcacheKey, &statusAPI); err == nil { response.WriteHeaderAndEntity(http.StatusOK, statusAPI) return } - q := datastore.NewQuery(statusDBEntity).Order("-ChangeDate").Limit(1) + q := DB.NewQuery(statusDBEntity).Order("-ChangeDate").Limit(1) var statusOnDBList []StatusEntity k, err := q.GetAll(ctx, &statusOnDBList) @@ -250,11 +245,8 @@ func getCurrentStatus(request *restful.Request, response *restful.Response) { statusAPI.Id = k[0].IntID() // add to memcache / overwrite existing / ignore errors - item := &memcache.Item{ - Key: statusMemcacheKey, - Object: statusAPI, - } - memcache.Gob.Set(ctx, item) + // add to memcache / overwrite existing / ignore errors + DB.CacheSet(ctx, statusMemcacheKey, statusAPI) response.WriteHeaderAndEntity(http.StatusOK, statusAPI) } @@ -269,9 +261,9 @@ func getStatusTextById(request *restful.Request, response *restful.Response) { return } - statusKey := datastore.NewKey(ctx, statusDBEntity, "", i, statusEntityRootKey(ctx)) + statusKey := DB.NewKey(ctx, statusDBEntity, "", i, statusEntityRootKey(ctx)) - q := datastore.NewQuery(statusDBEntityText).Ancestor(statusKey).Limit(1) // we have max. 1 Text per status + q := DB.NewQuery(statusDBEntityText).Ancestor(statusKey).Limit(1) // we have max. 1 Text per status var statusTextOnDBList []StatusEntityText k, err := q.GetAll(ctx, &statusTextOnDBList) @@ -301,13 +293,12 @@ func getStatusTextById(request *restful.Request, response *restful.Response) { func internalGetCurrentStatus(ctx context.Context) int { // first check Memcache - if item, err := memcache.Get(ctx, statusMemcacheKey); err == nil { - if i64, err := strconv.ParseInt(string(item.Value), 10, 0); err == nil { - return int(i64) - } + var itemValue int64 + if err := DB.CacheGet(ctx, statusMemcacheKey, &itemValue); err == nil { + return int(itemValue) } - q := datastore.NewQuery(statusDBEntity).Order("-ChangeDate").Limit(1) + q := DB.NewQuery(statusDBEntity).Order("-ChangeDate").Limit(1) var statusOnDBList []StatusEntity _, err := q.GetAll(ctx, &statusOnDBList) @@ -318,6 +309,3 @@ func internalGetCurrentStatus(ctx context.Context) int { return statusOnDBList[0].Status } - - - diff --git a/entity_telemetry.go b/entity_telemetry.go index 9e08d2d..293fcbb 100644 --- a/entity_telemetry.go +++ b/entity_telemetry.go @@ -22,7 +22,8 @@ import ( "net/http" "time" - "golang.org/x/net/context" + "context" + "google.golang.org/appengine" "google.golang.org/appengine/datastore" @@ -34,12 +35,12 @@ import ( // ---------------------------------------------------------------------------------------------------------------// type TelemetryEntity struct { Country string - Region string `datastore:",noindex"` - City string `datastore:",noindex"` - CityLatLong string `datastore:",noindex"` + Region string `datastore:",noindex"` + City string `datastore:",noindex"` + CityLatLong string `datastore:",noindex"` CreateDate time.Time LastChange time.Time - UseCount int64 `datastore:",noindex"` + UseCount int64 `datastore:",noindex"` OS string GCVersion string } @@ -105,7 +106,7 @@ func mapDBtoAPITelemetry(db *TelemetryEntity, api *TelemetryEntityGetAPIv1) { // telemetryEntityKey returns the key used for all telemetryEntity entries. func telemetryEntityRootKey(ctx context.Context) *datastore.Key { - return datastore.NewKey(ctx, telemetryDBEntity, telemetryDBEntityRootKey, 0, nil) + return DB.NewKey(ctx, telemetryDBEntity, telemetryDBEntityRootKey, 0, nil) } // ---------------------------------------------------------------------------------------------------------------// @@ -129,10 +130,10 @@ func upsertTelemetry(request *restful.Request, response *restful.Response) { // the only consumer of the APIs - any checks/response are to support this use-case // read if there is an entry existing for this IP Address - key := datastore.NewKey(ctx, telemetryDBEntity, telemetry.UserKey, 0, telemetryEntityRootKey(ctx)) + key := DB.NewKey(ctx, telemetryDBEntity, telemetry.UserKey, 0, telemetryEntityRootKey(ctx)) currentTelemetry := new(TelemetryEntity) - err := datastore.Get(ctx, key, currentTelemetry) + err := DB.Get(ctx, key, currentTelemetry) if err == nil { // entry found, increment counter currentTelemetry.UseCount += telemetry.Increment @@ -148,7 +149,7 @@ func upsertTelemetry(request *restful.Request, response *restful.Response) { // general mapping mapAPItoDBTelemetry(telemetry, currentTelemetry) - if _, err := datastore.Put(ctx, key, currentTelemetry); err != nil { + if _, err := DB.Put(ctx, key, currentTelemetry); err != nil { if appengine.IsOverQuota(err) { // return 503 and a text similar to what GAE is returning as well addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota") @@ -160,13 +161,12 @@ func upsertTelemetry(request *restful.Request, response *restful.Response) { response.WriteHeaderAndEntity(http.StatusCreated, currentTelemetry) - } func getTelemetry(request *restful.Request, response *restful.Response) { ctx := appengine.NewContext(request.Request) - oldestDate := time.Date(2000, time.January, 1,0,0,0,0, time.UTC) + oldestDate := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) var createdAfter time.Time var updatedAfter time.Time var err error @@ -193,21 +193,21 @@ func getTelemetry(request *restful.Request, response *restful.Response) { // only one query parameter is processed on the request in case of multiple parameters, // follow the priority given by the sequence below (and ignore the other parameters) - var q* datastore.Query + var q Query if createdAfter != oldestDate { - q = datastore.NewQuery(telemetryDBEntity). + q = DB.NewQuery(telemetryDBEntity). Filter("CreateDate >=", createdAfter) } else if updatedAfter != oldestDate { - q = datastore.NewQuery(telemetryDBEntity). + q = DB.NewQuery(telemetryDBEntity). Filter("LastChange >=", updatedAfter) } else if os != "" { - q = datastore.NewQuery(telemetryDBEntity). + q = DB.NewQuery(telemetryDBEntity). Filter("OS =", os) } else if version != "" { - q = datastore.NewQuery(telemetryDBEntity). + q = DB.NewQuery(telemetryDBEntity). Filter("GCVersion =", version) } else { - q = datastore.NewQuery(telemetryDBEntity) + q = DB.NewQuery(telemetryDBEntity) } var telemetryList TelemetryEntityGetAPIv1List diff --git a/entity_usermetric.go b/entity_usermetric.go index 8a34dca..c768aaa 100644 --- a/entity_usermetric.go +++ b/entity_usermetric.go @@ -15,39 +15,38 @@ * along with this program. If not, see . */ - package main import ( - "google.golang.org/appengine/log" + "fmt" "net/http" - "time" "strconv" - "fmt" + "time" + + "google.golang.org/appengine/log" + + "context" - "golang.org/x/net/context" "google.golang.org/appengine" "google.golang.org/appengine/datastore" "github.com/emicklei/go-restful" ) - // ---------------------------------------------------------------------------------------------------------------// // Full Golden Cheetah usermetric definition (usermetricentity) which is stored in DB // ---------------------------------------------------------------------------------------------------------------// type UserMetricEntity struct { Header CommonEntityHeader - MetricXML string `datastore:",noindex"` - CreatorNick string `datastore:",noindex"` - CreatorEmail string `datastore:",noindex"` + MetricXML string `datastore:",noindex"` + CreatorNick string `datastore:",noindex"` + CreatorEmail string `datastore:",noindex"` } type UserMetricEntityHeaderOnly struct { Header CommonEntityHeader } - // ---------------------------------------------------------------------------------------------------------------// // API View Definition // ---------------------------------------------------------------------------------------------------------------// @@ -55,9 +54,9 @@ type UserMetricEntityHeaderOnly struct { // Full structure for GET and PUT type UserMetricAPIv1 struct { Header CommonAPIHeaderV1 `json:"header"` - MetricXML string `json:"metrictxml"` - CreatorNick string `json:"creatorNick"` - CreatorEmail string `json:"creatorEmail"` + MetricXML string `json:"metrictxml"` + CreatorNick string `json:"creatorNick"` + CreatorEmail string `json:"creatorEmail"` } type UserMetricAPIv1List []UserMetricAPIv1 @@ -68,8 +67,6 @@ type UserMetricAPIv1HeaderOnly struct { } type UserMetricAPIv1HeaderOnlyList []UserMetricAPIv1HeaderOnly - - // ---------------------------------------------------------------------------------------------------------------// // Data Storage View // ---------------------------------------------------------------------------------------------------------------// @@ -84,21 +81,18 @@ func mapAPItoDBUserMetric(api *UserMetricAPIv1, db *UserMetricEntity) { db.CreatorEmail = api.CreatorEmail } - -func mapDBtoAPIUserMetric(db* UserMetricEntity, api *UserMetricAPIv1) { +func mapDBtoAPIUserMetric(db *UserMetricEntity, api *UserMetricAPIv1) { mapDBtoAPICommonHeader(&db.Header, &api.Header) api.MetricXML = db.MetricXML api.CreatorNick = db.CreatorNick api.CreatorEmail = db.CreatorEmail } - - // supporting functions // usermetricEntityKey returns the key used for all usermetricEntity entries. func usermetricEntityRootKey(ctx context.Context) *datastore.Key { - return datastore.NewKey(ctx, usermetricDBEntity, usermetricDBEntityRootKey, 0, nil) + return DB.NewKey(ctx, usermetricDBEntity, usermetricDBEntityRootKey, 0, nil) } // ---------------------------------------------------------------------------------------------------------------// @@ -106,7 +100,7 @@ func usermetricEntityRootKey(ctx context.Context) *datastore.Key { // ---------------------------------------------------------------------------------------------------------------// func insertUserMetric(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() metric := new(UserMetricAPIv1) if err := request.ReadEntity(metric); err != nil { @@ -126,7 +120,7 @@ func insertUserMetric(request *restful.Request, response *restful.Response) { metricDB.Header.Deleted = false // auto-curate if a registered "curator" is adding user metric - curatorQuery := datastore.NewQuery(curatorDBEntity).Filter("CuratorId =", metricDB.Header.CreatorId) + curatorQuery := DB.NewQuery(curatorDBEntity).Filter("CuratorId =", metricDB.Header.CreatorId) counter, _ := curatorQuery.Count(ctx) // ignore errors/just leave uncurated if counter == 1 { metricDB.Header.Curated = true @@ -135,10 +129,10 @@ func insertUserMetric(request *restful.Request, response *restful.Response) { } // and now store it - key := datastore.NewIncompleteKey(ctx, usermetricDBEntity, usermetricEntityRootKey(ctx)) - key, err := datastore.Put(ctx, key, metricDB) + key := DB.NewIncompleteKey(ctx, usermetricDBEntity, usermetricEntityRootKey(ctx)) + key, err := DB.Put(ctx, key, metricDB) if err != nil { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } @@ -148,7 +142,7 @@ func insertUserMetric(request *restful.Request, response *restful.Response) { } func updateUserMetric(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() metric := new(UserMetricAPIv1) if err := request.ReadEntity(metric); err != nil { @@ -170,9 +164,9 @@ func updateUserMetric(request *restful.Request, response *restful.Response) { // and now store it - key := datastore.NewKey(ctx, usermetricDBEntity, "", metric.Header.Id, usermetricEntityRootKey(ctx)) - if _, err := datastore.Put(ctx, key, metricDB); err != nil { - commonResponseErrorProcessing (response, err) + key := DB.NewKey(ctx, usermetricDBEntity, "", metric.Header.Id, usermetricEntityRootKey(ctx)) + if _, err := DB.Put(ctx, key, metricDB); err != nil { + commonResponseErrorProcessing(response, err) return } @@ -181,7 +175,7 @@ func updateUserMetric(request *restful.Request, response *restful.Response) { } func getUserMetricHeader(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() var date time.Time var err error @@ -196,16 +190,16 @@ func getUserMetricHeader(request *restful.Request, response *restful.Response) { date = time.Time{} } - const maxNumberOfHeadersPerCall = 200; // this has to be equal to GoldenCheetah - CloudDBUserMetric class + const maxNumberOfHeadersPerCall = 200 // this has to be equal to GoldenCheetah - CloudDBUserMetric class - q := datastore.NewQuery(usermetricDBEntity).Filter("Header.LastChanged >=", date).Order("Header.LastChanged").Limit(maxNumberOfHeadersPerCall) + q := DB.NewQuery(usermetricDBEntity).Filter("Header.LastChanged >=", date).Order("Header.LastChanged").Limit(maxNumberOfHeadersPerCall) var metricHeaderList UserMetricAPIv1HeaderOnlyList var metricsOnDBList []UserMetricEntityHeaderOnly k, err := q.GetAll(ctx, &metricsOnDBList) if err != nil && !isErrFieldMismatch(err) { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } @@ -218,14 +212,14 @@ func getUserMetricHeader(request *restful.Request, response *restful.Response) { } // write Info Log - log.Infof(ctx, "GetHeader from: %s", dateString ) + log.Infof(ctx, "GetHeader from: %s", dateString) response.WriteHeaderAndEntity(http.StatusOK, metricHeaderList) } func getUserMetricHeaderCount(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() var date time.Time var err error @@ -239,7 +233,7 @@ func getUserMetricHeaderCount(request *restful.Request, response *restful.Respon date = time.Time{} } - q := datastore.NewQuery(usermetricDBEntity).Filter("Header.LastChanged >=", date).Order("-Header.LastChanged") + q := DB.NewQuery(usermetricDBEntity).Filter("Header.LastChanged >=", date).Order("-Header.LastChanged") counter, _ := q.Count(ctx) response.WriteHeaderAndEntity(http.StatusOK, counter) @@ -247,29 +241,28 @@ func getUserMetricHeaderCount(request *restful.Request, response *restful.Respon } func getUserMetricById(request *restful.Request, response *restful.Response) { - ctx := appengine.NewContext(request.Request) + ctx := request.Request.Context() id := request.PathParameter("id") i, err := strconv.ParseInt(id, 10, 64) if err != nil { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } - key := datastore.NewKey(ctx, usermetricDBEntity, "", i, usermetricEntityRootKey(ctx)) + key := DB.NewKey(ctx, usermetricDBEntity, "", i, usermetricEntityRootKey(ctx)) metricDB := new(UserMetricEntity) - err = datastore.Get(ctx, key, metricDB) + err = DB.Get(ctx, key, metricDB) if err != nil && !isErrFieldMismatch(err) { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } - // now map and respond metric := new(UserMetricAPIv1) mapDBtoAPIUserMetric(metricDB, metric) - metric.Header.Id= key.IntID() + metric.Header.Id = key.IntID() response.WriteHeaderAndEntity(http.StatusOK, metric) } @@ -295,7 +288,7 @@ func curateUserMetricById(request *restful.Request, response *restful.Response) // ------------------- supporting functions ------------------------------------------------ func changeUserMetricById(request *restful.Request, response *restful.Response, changeDeleted bool, changeCurated bool, newStatus bool) { - c := appengine.NewContext(request.Request) + c := request.Request.Context() id := request.PathParameter("id") i, err := strconv.ParseInt(id, 10, 64) @@ -304,12 +297,12 @@ func changeUserMetricById(request *restful.Request, response *restful.Response, return } - key := datastore.NewKey(c, usermetricDBEntity, "", i, usermetricEntityRootKey(c)) + key := DB.NewKey(c, usermetricDBEntity, "", i, usermetricEntityRootKey(c)) metricDB := new(UserMetricEntity) - err = datastore.Get(c, key, metricDB) + err = DB.Get(c, key, metricDB) if err != nil && !isErrFieldMismatch(err) { - commonResponseErrorProcessing (response, err) + commonResponseErrorProcessing(response, err) return } @@ -328,7 +321,7 @@ func changeUserMetricById(request *restful.Request, response *restful.Response, metricDB.Header.LastChanged = time.Now() } - if _, err := datastore.Put(c, key, metricDB); err != nil { + if _, err := DB.Put(c, key, metricDB); err != nil { if appengine.IsOverQuota(err) { // return 503 and a text similar to what GAE is returning as well addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota") @@ -342,4 +335,3 @@ func changeUserMetricById(request *restful.Request, response *restful.Response, response.WriteHeaderAndEntity(http.StatusNoContent, "") } - diff --git a/entity_version.go b/entity_version.go index dfd4104..9cdd606 100644 --- a/entity_version.go +++ b/entity_version.go @@ -15,43 +15,41 @@ * along with this program. If not, see . */ - package main import ( + "fmt" "net/http" "strconv" - "fmt" - "golang.org/x/net/context" + "context" + "google.golang.org/appengine" "google.golang.org/appengine/datastore" "github.com/emicklei/go-restful" ) - // ---------------------------------------------------------------------------------------------------------------// // Golden Cheetah curator (versionentity) which is stored in DB // ---------------------------------------------------------------------------------------------------------------// type VersionEntity struct { Version int - Type int `datastore:",noindex"` - URL string `datastore:",noindex"` - Text string `datastore:",noindex"` - VersionText string `datastore:",noindex"` + Type int `datastore:",noindex"` + URL string `datastore:",noindex"` + Text string `datastore:",noindex"` + VersionText string `datastore:",noindex"` } // Constants defined for documentation purposes - as they are set by GC const ( - Version_Release = 10 + Version_Release = 10 Version_Release_Candidate = 20 Version_Development_Build = 30 ) - type VersionEntityText struct { - Text string `datastore:",noindex"` + Text string `datastore:",noindex"` } // ---------------------------------------------------------------------------------------------------------------// @@ -60,26 +58,24 @@ type VersionEntityText struct { // Full structure for POST/PUT type VersionEntityPostAPIv1 struct { - Version int `json:"version"` - Type int `json:"releaseType"` - URL string `json:"downloadURL"` - VersionText string `json:"versionText"` - Text string `json:"text"` + Version int `json:"version"` + Type int `json:"releaseType"` + URL string `json:"downloadURL"` + VersionText string `json:"versionText"` + Text string `json:"text"` } type VersionEntityGetAPIv1 struct { - Id int64 `json:"id"` - Version int `json:"version"` - Type int `json:"releaseType"` - URL string `json:"downloadURL"` - VersionText string `json:"versionText"` - Text string `json:"text"` + Id int64 `json:"id"` + Version int `json:"version"` + Type int `json:"releaseType"` + URL string `json:"downloadURL"` + VersionText string `json:"versionText"` + Text string `json:"text"` } - type VersionEntityGetAPIv1List []VersionEntityGetAPIv1 - // ---------------------------------------------------------------------------------------------------------------// // Data Storage View // ---------------------------------------------------------------------------------------------------------------// @@ -103,12 +99,11 @@ func mapDBtoAPIVersion(db *VersionEntity, api *VersionEntityGetAPIv1) { api.VersionText = db.VersionText } - // supporting functions // versionEntityKey returns the key used for all versionEntity entries. func versionEntityRootKey(ctx context.Context) *datastore.Key { - return datastore.NewKey(ctx, versionDBEntity, versionDBEntityRootKey, 0, nil) + return DB.NewKey(ctx, versionDBEntity, versionDBEntityRootKey, 0, nil) } // ---------------------------------------------------------------------------------------------------------------// @@ -131,8 +126,8 @@ func insertVersion(request *restful.Request, response *restful.Response) { mapAPItoDBVersion(version, versionDB) // and now store it - key := datastore.NewIncompleteKey(ctx, versionDBEntity, versionEntityRootKey(ctx)) - key, err := datastore.Put(ctx, key, versionDB) + key := DB.NewIncompleteKey(ctx, versionDBEntity, versionEntityRootKey(ctx)) + key, err := DB.Put(ctx, key, versionDB) if err != nil { if appengine.IsOverQuota(err) { // return 503 and a text similar to what GAE is returning as well @@ -160,7 +155,7 @@ func getVersion(request *restful.Request, response *restful.Response) { } } - q := datastore.NewQuery(versionDBEntity).Filter("Version >", version).Order("-Version") + q := DB.NewQuery(versionDBEntity).Filter("Version >", version).Order("-Version") var versionList VersionEntityGetAPIv1List @@ -192,7 +187,7 @@ func getLatestVersion(request *restful.Request, response *restful.Response) { var versionAPI VersionEntityGetAPIv1 - q := datastore.NewQuery(versionDBEntity).Order("-Version").Limit(1) + q := DB.NewQuery(versionDBEntity).Order("-Version").Limit(1) var versionOnDBList []VersionEntity k, err := q.GetAll(ctx, &versionOnDBList) @@ -212,9 +207,3 @@ func getLatestVersion(request *restful.Request, response *restful.Response) { response.WriteHeaderAndEntity(http.StatusOK, versionAPI) } - - - - - - diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a57eac5 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module clouddb + +go 1.24.4 + +require ( + github.com/emicklei/go-restful v2.16.0+incompatible + golang.org/x/net v0.47.0 + google.golang.org/appengine v1.6.8 +) + +require ( + github.com/golang/protobuf v1.5.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + google.golang.org/protobuf v1.26.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7750687 --- /dev/null +++ b/go.sum @@ -0,0 +1,55 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful v2.16.0+incompatible h1:rgqiKNjTnFQA6kkhFe16D8epTksy9HQ1MyrbDXSdYhM= +github.com/emicklei/go-restful v2.16.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/goldencheetahCloudDB.go b/goldencheetahCloudDB.go index f977272..a2f32da 100644 --- a/goldencheetahCloudDB.go +++ b/goldencheetahCloudDB.go @@ -29,12 +29,18 @@ import ( func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } appengine.Main() - } +var DB Store + // init the Webserver within the GAE framework func init() { + DB = &DatastoreStore{} ws := new(restful.WebService) @@ -42,186 +48,185 @@ func init() { // setup the gcharts endpoints - processing see "entity_gcharts.go" // ---------------------------------------------------------------------------------- ws. - Path("/v1"). - Doc("Manage GCharts"). - Consumes(restful.MIME_JSON). - Produces(restful.MIME_JSON) // you can specify this per route as well + Path("/v1"). + Doc("Manage GCharts"). + Consumes(restful.MIME_JSON). + Produces(restful.MIME_JSON) // you can specify this per route as well ws.Route(ws.POST("/gchart/").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(insertGChart). - // docs - Doc("creates a gchart"). - Operation("createGChart"). - Reads(GChartPostAPIv1{})) // from the request + // docs + Doc("creates a gchart"). + Operation("createGChart"). + Reads(GChartPostAPIv1{})) // from the request ws.Route(ws.PUT("/gchart/").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(updateGChart). - // docs - Doc("updates a gchart"). - Operation("updatedGChart"). - Reads(GChartPostAPIv1{})) // from the request + // docs + Doc("updates a gchart"). + Operation("updatedGChart"). + Reads(GChartPostAPIv1{})) // from the request ws.Route(ws.GET("/gchart/{id}").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(getGChartById). - // docs - Doc("get a gchart"). - Operation("getGChartbyId"). - Param(ws.PathParameter("id", "identifier of the gchart").DataType("string")). - Writes(GChartGetAPIv1{})) // on the response + // docs + Doc("get a gchart"). + Operation("getGChartbyId"). + Param(ws.PathParameter("id", "identifier of the gchart").DataType("string")). + Writes(GChartGetAPIv1{})) // on the response ws.Route(ws.PUT("/gchartuse/{id}").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(incrementGChartUsageById). - // docs - Doc("increments the DL use counter for a chart by 1"). - Operation("incrementUsageCounterById"). - Param(ws.PathParameter("id", "identifier of the gchart").DataType("string"))) + // docs + Doc("increments the DL use counter for a chart by 1"). + Operation("incrementUsageCounterById"). + Param(ws.PathParameter("id", "identifier of the gchart").DataType("string"))) ws.Route(ws.DELETE("/gchart/{id}").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(deleteGChartById). - // docs - Doc("delete a gchart by setting the deleted status"). - Operation("deleteGChartbyId"). - Param(ws.PathParameter("id", "identifier of the chart").DataType("string"))) + // docs + Doc("delete a gchart by setting the deleted status"). + Operation("deleteGChartbyId"). + Param(ws.PathParameter("id", "identifier of the chart").DataType("string"))) ws.Route(ws.PUT("/gchartcuration/{id}").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(curateGChartById). - // docs - Doc("set the curation status of the gchart to {newStatus} which must be 'true' or 'false' "). - Operation("updateGChartCurationStatus"). - Param(ws.PathParameter("id", "identifier of the gchart").DataType("string")). - Param(ws.QueryParameter("newStatus", "true/false curation status").DataType("bool"))) + // docs + Doc("set the curation status of the gchart to {newStatus} which must be 'true' or 'false' "). + Operation("updateGChartCurationStatus"). + Param(ws.PathParameter("id", "identifier of the gchart").DataType("string")). + Param(ws.QueryParameter("newStatus", "true/false curation status").DataType("bool"))) // Endpoint for GChartHeader only (no JPG or Definition) ws.Route(ws.GET("/gchartheader").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(getGChartHeader). - // docs - Doc("gets a collection of gcharts header - in buckets of x charts - table sort is new to old"). - Operation("getGChartHeader"). - Param(ws.QueryParameter("dateFrom", "Date of last change").DataType("string")). - Writes(GChartAPIv1HeaderOnlyList{})) // on the response + // docs + Doc("gets a collection of gcharts header - in buckets of x charts - table sort is new to old"). + Operation("getGChartHeader"). + Param(ws.QueryParameter("dateFrom", "Date of last change").DataType("string")). + Writes(GChartAPIv1HeaderOnlyList{})) // on the response // Count Chart Headers to be retrieved ws.Route(ws.GET("/gchartheader/count").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(getGChartHeaderCount). - // docs - Doc("gets the number of gchart headers for testing,... selection"). - Operation("getGChartHeader"). - Param(ws.QueryParameter("dateFrom", "Date of last change").DataType("string"))) - + // docs + Doc("gets the number of gchart headers for testing,... selection"). + Operation("getGChartHeader"). + Param(ws.QueryParameter("dateFrom", "Date of last change").DataType("string"))) // ---------------------------------------------------------------------------------- // setup the usermetric endpoints - processing see "entity_usermetric.go" // ---------------------------------------------------------------------------------- ws. - Path("/v1"). - Doc("Manage User Metrics"). - Consumes(restful.MIME_JSON). - Produces(restful.MIME_JSON) // you can specify this per route as well + Path("/v1"). + Doc("Manage User Metrics"). + Consumes(restful.MIME_JSON). + Produces(restful.MIME_JSON) // you can specify this per route as well ws.Route(ws.POST("/usermetric/").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(insertUserMetric). - // docs - Doc("creates a usermetric"). - Operation("createUserMetric"). - Reads(UserMetricAPIv1{})) // from the request + // docs + Doc("creates a usermetric"). + Operation("createUserMetric"). + Reads(UserMetricAPIv1{})) // from the request ws.Route(ws.PUT("/usermetric/").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(updateUserMetric). - // docs - Doc("updates a usermetric"). - Operation("updateUserMetric"). - Reads(UserMetricAPIv1{})) // from the request + // docs + Doc("updates a usermetric"). + Operation("updateUserMetric"). + Reads(UserMetricAPIv1{})) // from the request ws.Route(ws.GET("/usermetric/{id}").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(getUserMetricById). - // docs - Doc("get a usermetric"). - Operation("getUserMetricbyId"). - Param(ws.PathParameter("id", "identifier of the user metric").DataType("string")). - Writes(UserMetricAPIv1{})) // on the response + // docs + Doc("get a usermetric"). + Operation("getUserMetricbyId"). + Param(ws.PathParameter("id", "identifier of the user metric").DataType("string")). + Writes(UserMetricAPIv1{})) // on the response ws.Route(ws.DELETE("/usermetric/{id}").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(deleteUserMetricById). - // docs - Doc("delete a usermetric by setting the deleted status"). - Operation("deleteUserMetricbyId"). - Param(ws.PathParameter("id", "identifier of the usermetric").DataType("string"))) + // docs + Doc("delete a usermetric by setting the deleted status"). + Operation("deleteUserMetricbyId"). + Param(ws.PathParameter("id", "identifier of the usermetric").DataType("string"))) ws.Route(ws.PUT("/usermetriccuration/{id}").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(curateUserMetricById). - // docs - Doc("set the curation status of the usermetric to {newStatus} which must be 'true' or 'false' "). - Operation("updateUserMetricCurationStatus"). - Param(ws.PathParameter("id", "identifier of the usermetric").DataType("string")). - Param(ws.QueryParameter("newStatus", "true/false curation status").DataType("bool"))) + // docs + Doc("set the curation status of the usermetric to {newStatus} which must be 'true' or 'false' "). + Operation("updateUserMetricCurationStatus"). + Param(ws.PathParameter("id", "identifier of the usermetric").DataType("string")). + Param(ws.QueryParameter("newStatus", "true/false curation status").DataType("bool"))) // Endpoint for Header only ws.Route(ws.GET("/usermetricheader").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(getUserMetricHeader). - // docs - Doc("gets a collection of usermetric header - in buckets of x headers - table sort is new to old"). - Operation("getUserMetricHeader"). - Param(ws.QueryParameter("dateFrom", "Date of last change").DataType("string")). - Writes(UserMetricAPIv1HeaderOnlyList{})) // on the response + // docs + Doc("gets a collection of usermetric header - in buckets of x headers - table sort is new to old"). + Operation("getUserMetricHeader"). + Param(ws.QueryParameter("dateFrom", "Date of last change").DataType("string")). + Writes(UserMetricAPIv1HeaderOnlyList{})) // on the response // Count Chart Headers to be retrieved ws.Route(ws.GET("/usermetricheader/count").Filter(basicAuthenticate).Filter(filterCloudDBStatus).To(getUserMetricHeaderCount). - // docs - Doc("gets the number of usermetric headers for testing,... selection"). - Operation("getUserMetricHeader"). - Param(ws.QueryParameter("dateFrom", "Date of last change").DataType("string"))) + // docs + Doc("gets the number of usermetric headers for testing,... selection"). + Operation("getUserMetricHeader"). + Param(ws.QueryParameter("dateFrom", "Date of last change").DataType("string"))) // ---------------------------------------------------------------------------------- // setup the curator endpoints - processing see "entity_curator.go" // ---------------------------------------------------------------------------------- ws.Route(ws.GET("/curator").Filter(basicAuthenticate).To(getCurator). - // docs - Doc("gets a collection of curators"). - Operation("getCurator"). - Param(ws.QueryParameter("curatorId", "UUid of the Curator").DataType("string")). - Writes(CuratorAPIv1List{})) // on the response + // docs + Doc("gets a collection of curators"). + Operation("getCurator"). + Param(ws.QueryParameter("curatorId", "UUid of the Curator").DataType("string")). + Writes(CuratorAPIv1List{})) // on the response ws.Route(ws.POST("/curator").Filter(basicAuthenticate).To(insertCurator). - // docs - Doc("creates a curator"). - Operation("createCurator"). - Reads(CuratorAPIv1{})) // from the request + // docs + Doc("creates a curator"). + Operation("createCurator"). + Reads(CuratorAPIv1{})) // from the request // ---------------------------------------------------------------------------------- // setup the status endpoints - processing see "entity_status.go" // ---------------------------------------------------------------------------------- ws.Route(ws.POST("/status").Filter(basicAuthenticate).To(insertStatus). - // docs - Doc("creates a new status entity"). - Operation("createStatus"). - Reads(StatusEntityPostAPIv1{})) // from the request + // docs + Doc("creates a new status entity"). + Operation("createStatus"). + Reads(StatusEntityPostAPIv1{})) // from the request ws.Route(ws.GET("/status").Filter(basicAuthenticate).To(getStatus). - // docs - Doc("gets a collection of status"). - Operation("getStatus"). - Param(ws.QueryParameter("dateFrom", "Status Validity").DataType("string")). - Writes(StatusEntityGetAPIv1List{})) // on the response + // docs + Doc("gets a collection of status"). + Operation("getStatus"). + Param(ws.QueryParameter("dateFrom", "Status Validity").DataType("string")). + Writes(StatusEntityGetAPIv1List{})) // on the response ws.Route(ws.GET("/status/latest").Filter(basicAuthenticate).To(getCurrentStatus). - // docs - Doc("gets the current/latest status"). - Operation("getStatus"). - Writes(StatusEntityGetAPIv1{})) // on the response + // docs + Doc("gets the current/latest status"). + Operation("getStatus"). + Writes(StatusEntityGetAPIv1{})) // on the response ws.Route(ws.GET("/statustext/{id}").Filter(basicAuthenticate).To(getStatusTextById). - // docs - Doc("gets the text for a specific status entity"). - Operation("getStatusText"). - Param(ws.PathParameter("id", "identifier of the version text").DataType("string")). - Writes(StatusEntityGetTextAPIv1{})) // on the response + // docs + Doc("gets the text for a specific status entity"). + Operation("getStatusText"). + Param(ws.PathParameter("id", "identifier of the version text").DataType("string")). + Writes(StatusEntityGetTextAPIv1{})) // on the response // ---------------------------------------------------------------------------------- // setup the version endpoints - processing see "entity_version.go" // ---------------------------------------------------------------------------------- ws.Route(ws.POST("/version").Filter(basicAuthenticate).To(insertVersion). - // docs + // docs Doc("creates a new version entity"). Operation("createVersion"). Reads(VersionEntityPostAPIv1{})) // from the request ws.Route(ws.GET("/version").Filter(basicAuthenticate).To(getVersion). - // docs + // docs Doc("gets a collection of versions"). Operation("getVersion"). Param(ws.QueryParameter("version", "Version").DataType("string")). Writes(VersionEntityGetAPIv1List{})) // on the response ws.Route(ws.GET("/version/latest").Filter(basicAuthenticate).To(getLatestVersion). - // docs + // docs Doc("gets the latest version"). Operation("getVersion"). Writes(VersionEntityGetAPIv1{})) // on the response @@ -231,13 +236,13 @@ func init() { // ---------------------------------------------------------------------------------- ws.Route(ws.PUT("/telemetry").Filter(basicAuthenticate).To(upsertTelemetry). - // docs + // docs Doc("stores location,... of the call based on IP adress"). Operation("post telemetry data"). Reads(TelemetryEntityPostAPIv1{})) // from the request ws.Route(ws.GET("/telemetry").Filter(basicAuthenticate).To(getTelemetry). - // docs + // docs Doc("gets a collection of versions"). Operation("get All Telemetry Data"). Param(ws.QueryParameter("createdAfter", "Telemetry created after").DataType("string")). @@ -252,7 +257,6 @@ func init() { } // init() - // global declarations const basicauth = "Basic_Auth" const authorization = "Authorization" @@ -262,11 +266,10 @@ const ( ) const status_unprocessable = "Error - CloudDB Status does not allow processing the request" - func basicAuthenticate(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { headerClientId := req.Request.Header.Get(authorization) if secretClientId := os.Getenv(basicauth); secretClientId != "" { - if fmt.Sprint("Basic ",secretClientId) != headerClientId { + if fmt.Sprint("Basic ", secretClientId) != headerClientId { resp.AddHeader("WWW-Authenticate", "Basic realm=Protected Area") resp.WriteErrorString(http.StatusUnauthorized, "Not Authorized") return @@ -288,12 +291,12 @@ func filterCloudDBStatus(req *restful.Request, resp *restful.Response, chain *re return } + req.Request = req.Request.WithContext(ctx) chain.ProcessFilter(req, resp) } - // Convenience functions for error handling -func addPlainTextError( r *restful.Response, httpStatus int, errorReason string ) { +func addPlainTextError(r *restful.Response, httpStatus int, errorReason string) { r.AddHeader("Content-Type", "text/plain") r.WriteErrorString(httpStatus, errorReason) } diff --git a/handlers_test.go b/handlers_test.go new file mode 100644 index 0000000..7709572 --- /dev/null +++ b/handlers_test.go @@ -0,0 +1,238 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "unsafe" + + "context" + + "github.com/emicklei/go-restful" + "google.golang.org/appengine/datastore" +) + +type fakeKey struct { + kind string + stringID string + intID int64 + parent *datastore.Key + appID string + namespace string +} + +func makeKey(kind, stringID string, intID int64, parent *datastore.Key) *datastore.Key { + fk := fakeKey{ + kind: kind, + stringID: stringID, + intID: intID, + parent: parent, + appID: "test-app", + namespace: "", + } + return (*datastore.Key)(unsafe.Pointer(&fk)) +} + +func TestInsertGChart(t *testing.T) { + os.Setenv("APPLICATION_ID", "test-app") + os.Setenv("Basic_Auth", "secret") + + // Setup Mock Store + mockStore := &MockStore{ + PutFunc: func(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) { + return makeKey("gchartentity", "", 123, nil), nil + }, + NewQueryFunc: func(kind string) Query { + return &MockQuery{ + CountFunc: func(ctx context.Context) (int, error) { + return 0, nil + }, + GetAllFunc: func(ctx context.Context, dst interface{}) ([]*datastore.Key, error) { + return nil, fmt.Errorf("mock error") + }, + } + }, + NewKeyFunc: func(ctx context.Context, kind, stringID string, intID int64, parent *datastore.Key) *datastore.Key { + return makeKey(kind, stringID, intID, parent) + }, + NewIncompleteKeyFunc: func(ctx context.Context, kind string, parent *datastore.Key) *datastore.Key { + return makeKey(kind, "", 0, parent) + }, + CacheGetFunc: func(ctx context.Context, key string, dst interface{}) error { + return fmt.Errorf("mock error") + }, + } + DB = mockStore + + // Setup Request + chart := GChartPostAPIv1{ + Header: CommonAPIHeaderV1{ + Name: "Test Chart", + }, + ChartSport: "Bike", + } + body, _ := json.Marshal(chart) + req, _ := http.NewRequest("POST", "/v1/gchart/", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Basic secret") + + rw := httptest.NewRecorder() + + // Dispatch through DefaultContainer + restful.DefaultContainer.ServeHTTP(rw, req) + + // Check Response + if rw.Code != http.StatusCreated { + t.Errorf("Expected 201 Created, got %d. Body: %s", rw.Code, rw.Body.String()) + } +} + +func TestGetGChartById(t *testing.T) { + os.Setenv("APPLICATION_ID", "test-app") + os.Setenv("Basic_Auth", "secret") + + // Setup Mock Store + mockStore := &MockStore{ + GetFunc: func(ctx context.Context, key *datastore.Key, dst interface{}) error { + // Fill dst with dummy data + if chart, ok := dst.(*GChartEntity); ok { + chart.ChartSport = "Run" + return nil + } + return datastore.ErrNoSuchEntity + }, + NewKeyFunc: func(ctx context.Context, kind, stringID string, intID int64, parent *datastore.Key) *datastore.Key { + return makeKey(kind, stringID, intID, parent) + }, + NewQueryFunc: func(kind string) Query { + return &MockQuery{ + GetAllFunc: func(ctx context.Context, dst interface{}) ([]*datastore.Key, error) { + return nil, fmt.Errorf("mock error") + }, + } + }, + CacheGetFunc: func(ctx context.Context, key string, dst interface{}) error { + return fmt.Errorf("mock error") + }, + } + DB = mockStore + + // Setup Request + req, _ := http.NewRequest("GET", "/v1/gchart/123", nil) + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Basic secret") + + rw := httptest.NewRecorder() + + // Dispatch through DefaultContainer + restful.DefaultContainer.ServeHTTP(rw, req) + + // Check Response + if rw.Code != http.StatusOK { + t.Errorf("Expected 200 OK, got %d. Body: %s", rw.Code, rw.Body.String()) + } + + var respChart GChartGetAPIv1 + json.Unmarshal(rw.Body.Bytes(), &respChart) + if respChart.ChartSport != "Run" { + t.Errorf("Expected ChartSport 'Run', got '%s'", respChart.ChartSport) + } +} + +func TestUpsertTelemetry(t *testing.T) { + os.Setenv("APPLICATION_ID", "test-app") + os.Setenv("Basic_Auth", "secret") + + // Setup Mock Store + mockStore := &MockStore{ + PutFunc: func(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) { + return makeKey("telemetryentity", "test-key", 0, nil), nil + }, + GetFunc: func(ctx context.Context, key *datastore.Key, dst interface{}) error { + return datastore.ErrNoSuchEntity + }, + NewKeyFunc: func(ctx context.Context, kind, stringID string, intID int64, parent *datastore.Key) *datastore.Key { + return makeKey(kind, stringID, intID, parent) + }, + } + DB = mockStore + + // Setup Request + telemetry := TelemetryEntityPostAPIv1{ + UserKey: "test-key", + OS: "Linux", + GCVersion: "3.6", + Increment: 25, + LastChange: time.Now().Format(dateTimeLayout), + } + body, _ := json.Marshal(telemetry) + req, _ := http.NewRequest("PUT", "/v1/telemetry", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Basic secret") + + rw := httptest.NewRecorder() + + // Dispatch through DefaultContainer + restful.DefaultContainer.ServeHTTP(rw, req) + + // Check Response + if rw.Code != http.StatusCreated { + t.Errorf("Expected 201 Created, got %d. Body: %s", rw.Code, rw.Body.String()) + } +} + +func TestGetVersion(t *testing.T) { + os.Setenv("APPLICATION_ID", "test-app") + os.Setenv("Basic_Auth", "secret") + + // Setup Mock Store + mockStore := &MockStore{ + NewQueryFunc: func(kind string) Query { + return &MockQuery{ + GetAllFunc: func(ctx context.Context, dst interface{}) ([]*datastore.Key, error) { + // Populate dst with mock data + versions := dst.(*[]VersionEntity) + *versions = append(*versions, VersionEntity{ + Version: 3600, + Type: 10, + URL: "http://example.com", + Text: "Release", + VersionText: "3.6", + }) + return []*datastore.Key{makeKey("versionentity", "", 1, nil)}, nil + }, + } + }, + } + DB = mockStore + + // Setup Request + req, _ := http.NewRequest("GET", "/v1/version?version=3500", nil) + req.Header.Set("Authorization", "Basic secret") + + rw := httptest.NewRecorder() + + // Dispatch through DefaultContainer + restful.DefaultContainer.ServeHTTP(rw, req) + + // Check Response + if rw.Code != http.StatusOK { + t.Errorf("Expected 200 OK, got %d. Body: %s", rw.Code, rw.Body.String()) + } + + var versions []VersionEntityGetAPIv1 + json.Unmarshal(rw.Body.Bytes(), &versions) + if len(versions) != 1 { + t.Errorf("Expected 1 version, got %d", len(versions)) + } + if versions[0].Version != 3600 { + t.Errorf("Expected Version 3600, got %d", versions[0].Version) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..11fca2f --- /dev/null +++ b/main_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "testing" + + "github.com/emicklei/go-restful" +) + +func TestRoutesRegistered(t *testing.T) { + // init() is called automatically + + services := restful.RegisteredWebServices() + if len(services) == 0 { + t.Fatal("No services registered") + } + + foundV1 := false + for _, ws := range services { + if ws.RootPath() == "/v1" { + foundV1 = true + break + } + } + + if !foundV1 { + t.Error("Service /v1 not found") + } +} diff --git a/mapper_test.go b/mapper_test.go new file mode 100644 index 0000000..c4c3297 --- /dev/null +++ b/mapper_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "encoding/base64" + "reflect" + "testing" + "time" +) + +func TestCommonHeaderMapping(t *testing.T) { + now := time.Now().Truncate(time.Second) // Truncate to second as format might lose precision + nowStr := now.Format(dateTimeLayout) + // Re-parse to match the precision loss in string conversion + now, _ = time.Parse(dateTimeLayout, nowStr) + + api := &CommonAPIHeaderV1{ + Name: "Test Name", + Description: "Test Desc", + Language: "en", + GcVersion: "3.6", + LastChanged: nowStr, + CreatorId: "user1", + Curated: true, + Deleted: false, + } + + db := &CommonEntityHeader{} + mapAPItoDBCommonHeader(api, db) + + if db.Name != api.Name { + t.Errorf("Name mismatch: %s != %s", db.Name, api.Name) + } + if !db.LastChanged.Equal(now) { + t.Errorf("LastChanged mismatch: %v != %v", db.LastChanged, now) + } + + api2 := &CommonAPIHeaderV1{} + mapDBtoAPICommonHeader(db, api2) + + if api2.Name != api.Name { + t.Errorf("Reverse Name mismatch: %s != %s", api2.Name, api.Name) + } + if api2.LastChanged != api.LastChanged { + t.Errorf("Reverse LastChanged mismatch: %s != %s", api2.LastChanged, api.LastChanged) + } +} + +func TestGChartMapping(t *testing.T) { + imgData := []byte("fake image data") + imgStr := base64.StdEncoding.EncodeToString(imgData) + + api := &GChartPostAPIv1{ + Header: CommonAPIHeaderV1{ + Name: "Chart1", + }, + ChartSport: "Bike", + ChartType: "Plot", + ChartView: "View1", + ChartDef: "Def1", + Image: imgStr, + CreatorNick: "Nick", + CreatorEmail: "email@example.com", + } + + db := &GChartEntity{} + mapAPItoDBGChart(api, db) + + if db.ChartSport != api.ChartSport { + t.Errorf("Sport mismatch") + } + if !reflect.DeepEqual(db.Image, imgData) { + t.Errorf("Image mismatch") + } + + // Test Image Size Limit + largeImg := make([]byte, 1024001) + largeImgStr := base64.StdEncoding.EncodeToString(largeImg) + api.Image = largeImgStr + mapAPItoDBGChart(api, db) + if db.Image != nil { + t.Errorf("Image should be nil if too large") + } +} diff --git a/mock_store.go b/mock_store.go new file mode 100644 index 0000000..ed6048f --- /dev/null +++ b/mock_store.go @@ -0,0 +1,138 @@ +package main + +import ( + "reflect" + + "context" + + "google.golang.org/appengine/datastore" +) + +type MockStore struct { + PutFunc func(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) + GetFunc func(ctx context.Context, key *datastore.Key, dst interface{}) error + NewQueryFunc func(kind string) Query + CacheGetFunc func(ctx context.Context, key string, dst interface{}) error + CacheSetFunc func(ctx context.Context, key string, item interface{}) error + CacheFlushFunc func(ctx context.Context) error + NewKeyFunc func(ctx context.Context, kind, stringID string, intID int64, parent *datastore.Key) *datastore.Key + NewIncompleteKeyFunc func(ctx context.Context, kind string, parent *datastore.Key) *datastore.Key +} + +func (m *MockStore) Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) { + if m.PutFunc != nil { + return m.PutFunc(ctx, key, src) + } + return key, nil +} + +func (m *MockStore) Get(ctx context.Context, key *datastore.Key, dst interface{}) error { + if m.GetFunc != nil { + return m.GetFunc(ctx, key, dst) + } + return nil +} + +func (m *MockStore) NewQuery(kind string) Query { + if m.NewQueryFunc != nil { + return m.NewQueryFunc(kind) + } + return &MockQuery{} +} + +func (m *MockStore) CacheGet(ctx context.Context, key string, dst interface{}) error { + if m.CacheGetFunc != nil { + return m.CacheGetFunc(ctx, key, dst) + } + return nil +} + +func (m *MockStore) CacheSet(ctx context.Context, key string, item interface{}) error { + if m.CacheSetFunc != nil { + return m.CacheSetFunc(ctx, key, item) + } + return nil +} + +func (m *MockStore) CacheFlush(ctx context.Context) error { + if m.CacheFlushFunc != nil { + return m.CacheFlushFunc(ctx) + } + return nil +} + +func (m *MockStore) NewKey(ctx context.Context, kind, stringID string, intID int64, parent *datastore.Key) *datastore.Key { + if m.NewKeyFunc != nil { + return m.NewKeyFunc(ctx, kind, stringID, intID, parent) + } + // Default behavior: return a dummy key that doesn't panic + // We can't use datastore.NewKey here if it panics. + // But datastore.Key is opaque. We can construct one using reflection or just return nil? + // Returning nil might cause panic in Put/Get if they expect a key. + // Ideally we return a key that works with our MockStore. + // Since our MockStore intercepts Put/Get, we can handle nil keys or dummy keys. + return &datastore.Key{} +} + +func (m *MockStore) NewIncompleteKey(ctx context.Context, kind string, parent *datastore.Key) *datastore.Key { + if m.NewIncompleteKeyFunc != nil { + return m.NewIncompleteKeyFunc(ctx, kind, parent) + } + return &datastore.Key{} +} + +type MockQuery struct { + FilterFunc func(filterStr string, value interface{}) Query + OrderFunc func(fieldName string) Query + LimitFunc func(limit int) Query + AncestorFunc func(ancestor *datastore.Key) Query + CountFunc func(ctx context.Context) (int, error) + GetAllFunc func(ctx context.Context, dst interface{}) ([]*datastore.Key, error) +} + +func (mq *MockQuery) Filter(filterStr string, value interface{}) Query { + if mq.FilterFunc != nil { + return mq.FilterFunc(filterStr, value) + } + return mq +} + +func (mq *MockQuery) Order(fieldName string) Query { + if mq.OrderFunc != nil { + return mq.OrderFunc(fieldName) + } + return mq +} + +func (mq *MockQuery) Limit(limit int) Query { + if mq.LimitFunc != nil { + return mq.LimitFunc(limit) + } + return mq +} + +func (mq *MockQuery) Ancestor(ancestor *datastore.Key) Query { + if mq.AncestorFunc != nil { + return mq.AncestorFunc(ancestor) + } + return mq +} + +func (mq *MockQuery) Count(ctx context.Context) (int, error) { + if mq.CountFunc != nil { + return mq.CountFunc(ctx) + } + return 0, nil +} + +func (mq *MockQuery) GetAll(ctx context.Context, dst interface{}) ([]*datastore.Key, error) { + if mq.GetAllFunc != nil { + return mq.GetAllFunc(ctx, dst) + } + // Reflection to set dst to empty slice if needed + v := reflect.ValueOf(dst) + if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Slice { + v.Elem().Set(reflect.MakeSlice(v.Elem().Type(), 0, 0)) + } + return nil, nil +} diff --git a/store.go b/store.go new file mode 100644 index 0000000..d62f5f3 --- /dev/null +++ b/store.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + + "google.golang.org/appengine/datastore" + "google.golang.org/appengine/memcache" +) + +// Store defines the interface for database and cache operations. +type Store interface { + Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) + Get(ctx context.Context, key *datastore.Key, dst interface{}) error + NewQuery(kind string) Query + + // Cache operations + CacheGet(ctx context.Context, key string, dst interface{}) error + CacheSet(ctx context.Context, key string, item interface{}) error + CacheFlush(ctx context.Context) error + + // Key operations + NewKey(ctx context.Context, kind, stringID string, intID int64, parent *datastore.Key) *datastore.Key + NewIncompleteKey(ctx context.Context, kind string, parent *datastore.Key) *datastore.Key +} + +// Query defines the interface for datastore queries. +type Query interface { + Filter(filterStr string, value interface{}) Query + Order(fieldName string) Query + Limit(limit int) Query + Ancestor(ancestor *datastore.Key) Query + Count(ctx context.Context) (int, error) + GetAll(ctx context.Context, dst interface{}) ([]*datastore.Key, error) +} + +// DatastoreStore is the concrete implementation of Store using App Engine Datastore. +type DatastoreStore struct{} + +func (ds *DatastoreStore) Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) { + return datastore.Put(ctx, key, src) +} + +func (ds *DatastoreStore) Get(ctx context.Context, key *datastore.Key, dst interface{}) error { + return datastore.Get(ctx, key, dst) +} + +func (ds *DatastoreStore) NewQuery(kind string) Query { + return &DatastoreQuery{q: datastore.NewQuery(kind)} +} + +func (ds *DatastoreStore) CacheGet(ctx context.Context, key string, dst interface{}) error { + _, err := memcache.Gob.Get(ctx, key, dst) + return err +} + +func (ds *DatastoreStore) CacheSet(ctx context.Context, key string, item interface{}) error { + return memcache.Gob.Set(ctx, &memcache.Item{ + Key: key, + Object: item, + }) +} + +func (ds *DatastoreStore) CacheFlush(ctx context.Context) error { + return memcache.Flush(ctx) +} + +func (ds *DatastoreStore) NewKey(ctx context.Context, kind, stringID string, intID int64, parent *datastore.Key) *datastore.Key { + return datastore.NewKey(ctx, kind, stringID, intID, parent) +} + +func (ds *DatastoreStore) NewIncompleteKey(ctx context.Context, kind string, parent *datastore.Key) *datastore.Key { + return datastore.NewIncompleteKey(ctx, kind, parent) +} + +// DatastoreQuery is the concrete implementation of Query. +type DatastoreQuery struct { + q *datastore.Query +} + +func (dq *DatastoreQuery) Filter(filterStr string, value interface{}) Query { + dq.q = dq.q.Filter(filterStr, value) + return dq +} + +func (dq *DatastoreQuery) Order(fieldName string) Query { + dq.q = dq.q.Order(fieldName) + return dq +} + +func (dq *DatastoreQuery) Limit(limit int) Query { + dq.q = dq.q.Limit(limit) + return dq +} + +func (dq *DatastoreQuery) Ancestor(ancestor *datastore.Key) Query { + dq.q = dq.q.Ancestor(ancestor) + return dq +} + +func (dq *DatastoreQuery) Count(ctx context.Context) (int, error) { + return dq.q.Count(ctx) +} + +func (dq *DatastoreQuery) GetAll(ctx context.Context, dst interface{}) ([]*datastore.Key, error) { + return dq.q.GetAll(ctx, dst) +}