Added api funtionality
This commit is contained in:
parent
f6ce8578d2
commit
4d331ad1ad
5
Makefile
5
Makefile
@ -16,4 +16,7 @@ ps:
|
||||
docker compose ps
|
||||
|
||||
logs:
|
||||
docker compose logs -f
|
||||
docker compose logs -f
|
||||
|
||||
server:
|
||||
go -C api run ./cmd/server
|
||||
39
api/cmd/server/main.go
Normal file
39
api/cmd/server/main.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"example.com/bridging-sample/api/internal/config"
|
||||
"example.com/bridging-sample/api/internal/db"
|
||||
httpHandlers "example.com/bridging-sample/api/internal/http"
|
||||
"example.com/bridging-sample/api/internal/repo"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load("../.env", ".env")
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("config: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := db.NewClient(ctx, cfg.Project, cfg.Instance, cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatalf("spanner client: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
items := repo.NewItemRepo(client)
|
||||
h := &httpHandlers.Handlers{Items: items}
|
||||
|
||||
r := gin.Default()
|
||||
h.Register(r)
|
||||
|
||||
log.Println("listening on :8080")
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
61
api/go.mod
61
api/go.mod
@ -1,19 +1,45 @@
|
||||
module api-sample
|
||||
module example.com/bridging-sample/api
|
||||
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
cloud.google.com/go/spanner v1.84.1
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/google/uuid v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.121.4 // indirect
|
||||
cloud.google.com/go/auth v0.16.3 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@ -21,13 +47,34 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/api v0.244.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
1632
api/go.sum
1632
api/go.sum
File diff suppressed because it is too large
Load Diff
28
api/internal/config/config.go
Normal file
28
api/internal/config/config.go
Normal file
@ -0,0 +1,28 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Project string
|
||||
Instance string
|
||||
Database string
|
||||
Host string
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
c := Config{
|
||||
Project: os.Getenv("SPANNER_PROJECT"),
|
||||
Instance: os.Getenv("SPANNER_INSTANCE"),
|
||||
Database: os.Getenv("SPANNER_DATABASE"),
|
||||
Host: os.Getenv("SPANNER_EMULATOR_HOST"),
|
||||
}
|
||||
if c.Project == "" || c.Instance == "" || c.Database == "" {
|
||||
fmt.Printf("%v, %v, %v", c.Project, c.Instance, c.Database)
|
||||
fmt.Println()
|
||||
return c, fmt.Errorf("Missing .env credential")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
13
api/internal/db/spanner.go
Normal file
13
api/internal/db/spanner.go
Normal file
@ -0,0 +1,13 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"cloud.google.com/go/spanner"
|
||||
)
|
||||
|
||||
func NewClient(ctx context.Context, project, instance, database string) (*spanner.Client, error) {
|
||||
dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, database)
|
||||
return spanner.NewClient(ctx, dbPath)
|
||||
}
|
||||
69
api/internal/http/handlers.go
Normal file
69
api/internal/http/handlers.go
Normal file
@ -0,0 +1,69 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"example.com/bridging-sample/api/internal/models"
|
||||
"example.com/bridging-sample/api/internal/repo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handlers struct{ Items *repo.ItemRepo }
|
||||
|
||||
func (h *Handlers) Register(r *gin.Engine) {
|
||||
r.GET("/healthz", func(c *gin.Context) { c.String(http.StatusOK, "ok") })
|
||||
|
||||
r.GET("/items", h.listItems)
|
||||
r.GET("/items/:id", h.getItem)
|
||||
r.POST("/items", h.createItem)
|
||||
r.DELETE("/items/:id", h.deleteItem)
|
||||
r.GET("/", getHelloWorld)
|
||||
}
|
||||
|
||||
func (h *Handlers) listItems(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
items, err := h.Items.List(c, int32(limit))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
func getHelloWorld(c *gin.Context) {
|
||||
c.IndentedJSON(http.StatusOK, "Hello world")
|
||||
}
|
||||
|
||||
func (h *Handlers) getItem(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.Items.Get(c, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (h *Handlers) createItem(c *gin.Context) {
|
||||
var in models.Item
|
||||
if err := c.BindJSON(&in); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
id, err := h.Items.Create(c, in)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"id": id})
|
||||
}
|
||||
|
||||
func (h *Handlers) deleteItem(c *gin.Context) {
|
||||
if err := h.Items.Delete(c, c.Param("id")); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
9
api/internal/models/item.go
Normal file
9
api/internal/models/item.go
Normal file
@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
type Item struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Amount int64 `json:"amount"`
|
||||
Price float32 `json:"price"`
|
||||
SalesAmount int64 `json:"sales_amount"`
|
||||
}
|
||||
119
api/internal/repo/item_repo.go
Normal file
119
api/internal/repo/item_repo.go
Normal file
@ -0,0 +1,119 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"cloud.google.com/go/spanner"
|
||||
"example.com/bridging-sample/api/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
type ItemRepo struct {
|
||||
client *spanner.Client
|
||||
}
|
||||
|
||||
func NewItemRepo(c *spanner.Client) *ItemRepo { return &ItemRepo{client: c} }
|
||||
|
||||
// Create with UUID PK (fits the DDL Option B we discussed)
|
||||
func (r *ItemRepo) Create(ctx context.Context, m models.Item) (string, error) {
|
||||
id := uuid.NewString()
|
||||
stmt := spanner.Statement{
|
||||
SQL: `INSERT INTO item_stock (id, name, amount, price, sales_amount)
|
||||
VALUES (@id, @name, @amount, CAST(@price AS NUMERIC), @sales_amount)`,
|
||||
Params: map[string]interface{}{
|
||||
"id": id,
|
||||
"name": m.Name,
|
||||
"amount": m.Amount,
|
||||
"price": m.Price,
|
||||
"sales_amount": m.SalesAmount,
|
||||
},
|
||||
}
|
||||
_, err := r.client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error {
|
||||
return tx.BufferWrite([]*spanner.Mutation{
|
||||
spanner.InsertOrUpdateMap("item_stock", map[string]interface{}{
|
||||
"id": id,
|
||||
"name": m.Name,
|
||||
"amount": m.Amount,
|
||||
"price": m.Price,
|
||||
"sales_amount": m.SalesAmount,
|
||||
}),
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
// fallback to statement example:
|
||||
_, altErr := r.client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error {
|
||||
_, e := tx.Update(ctx, stmt)
|
||||
return e
|
||||
})
|
||||
if altErr != nil {
|
||||
return "", fmt.Errorf("create item: %w", err)
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (r *ItemRepo) Get(ctx context.Context, id string) (*models.Item, error) {
|
||||
stmt := spanner.Statement{
|
||||
SQL: `SELECT id, name, amount, CAST(price AS STRING) AS price, sales_amount
|
||||
FROM item_stock WHERE id = @id`,
|
||||
Params: map[string]interface{}{"id": id},
|
||||
}
|
||||
iter := r.client.Single().Query(ctx, stmt)
|
||||
defer iter.Stop()
|
||||
|
||||
row, err := iter.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m models.Item
|
||||
if err := row.Columns(&m.Id, &m.Name, &m.Amount, &m.Price, &m.SalesAmount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *ItemRepo) List(ctx context.Context, limit int32) ([]models.Item, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
stmt := spanner.Statement{
|
||||
SQL: fmt.Sprintf(`SELECT id, name, amount, CAST(price AS STRING) AS price, sales_amount
|
||||
FROM item_stock ORDER BY id DESC LIMIT %d`, limit),
|
||||
}
|
||||
it := r.client.Single().Query(ctx, stmt)
|
||||
defer it.Stop()
|
||||
|
||||
var out []models.Item
|
||||
for {
|
||||
row, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m models.Item
|
||||
if err := row.Columns(&m.Id, &m.Name, &m.Amount, &m.Price, &m.SalesAmount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *ItemRepo) Delete(ctx context.Context, id string) error {
|
||||
mut := spanner.Delete("item_stock", spanner.Key{id})
|
||||
_, err := r.client.Apply(ctx, []*spanner.Mutation{mut})
|
||||
return err
|
||||
}
|
||||
|
||||
// Example of running raw SQL (useful in emulator):
|
||||
func (r *ItemRepo) ExecSQL(ctx context.Context, sql string) error {
|
||||
_, err := r.client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error {
|
||||
_, e := tx.Update(ctx, spanner.Statement{SQL: sql})
|
||||
return e
|
||||
})
|
||||
return err
|
||||
}
|
||||
26
api/main.go
26
api/main.go
@ -1,26 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type salesDetails struct {
|
||||
Id string
|
||||
Name string
|
||||
Amount int64
|
||||
Price float32
|
||||
SalesAmount int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
router.GET("/", getHelloWorld)
|
||||
|
||||
router.Run("localhost:8080")
|
||||
}
|
||||
|
||||
func getHelloWorld(c *gin.Context) {
|
||||
c.IndentedJSON(http.StatusOK, "Hello world")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user