Added api funtionality

This commit is contained in:
Nik Afiq 2025-08-24 01:59:34 +09:00
parent f6ce8578d2
commit 4d331ad1ad
10 changed files with 1957 additions and 44 deletions

View File

@ -17,3 +17,6 @@ ps:
logs:
docker compose logs -f
server:
go -C api run ./cmd/server

39
api/cmd/server/main.go Normal file
View 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)
}
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View 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
}

View 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)
}

View 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)
}

View 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"`
}

View 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
}

View File

@ -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")
}