From 261361e5a753a01aaa320a6dcbe5f2dd76a47ad1 Mon Sep 17 00:00:00 2001 From: Tigor Hutasuhut Date: Tue, 6 Aug 2024 22:13:37 +0700 Subject: [PATCH] app: added one api connection between backend and frontend --- Makefile | 7 ++++ go.mod | 6 ++++ go.sum | 16 +++++++-- go/api/api.go | 2 +- go/api/devices_create.go | 2 +- go/api/devices_get_by_slug.go | 2 +- go/cmd/bluemage/main.go | 25 ++++++++++++-- go/cmd/bluemage/serve/serve.go | 63 ++++++++++++++++++++++++++++++++++ go/pkg/errs/errs.go | 8 +++++ go/server/cors.go | 21 ++++++++++++ go/server/interceptor.go | 39 +++++++++++++++++++++ schemas/proto/buf.gen.web.yaml | 4 +-- web/.env | 1 + web/src/App.svelte | 15 ++++++++ web/src/client/client.ts | 9 +++++ 15 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 go/cmd/bluemage/serve/serve.go create mode 100644 go/server/cors.go create mode 100644 go/server/interceptor.go create mode 100644 web/.env create mode 100644 web/src/client/client.ts diff --git a/Makefile b/Makefile index 073681c..4b4e1b6 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,13 @@ export GOOSE_MIGRATION_DIR ?= schemas/migrations build: generate-go generate-web go build -o bin/bluemage ./go/cmd/bluemage/main.go +run: build + ARGS=$${ARGS:-serve} + ./bin/bluemage $$ARGS + +run-web: generate-web + cd web && npm run dev + generate-go: migrate rm -rf go/gen (cd ./schemas/proto && buf generate --template buf.gen.go.yaml .) diff --git a/go.mod b/go.mod index 1254a13..bdb6a9e 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,15 @@ go 1.22.5 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240717164558-a6c49f84cc0f.2 connectrpc.com/connect v1.16.2 + connectrpc.com/cors v0.1.0 github.com/aarondl/opt v0.0.0-20240623220848-083f18ab9536 github.com/bufbuild/protovalidate-go v0.6.3 github.com/jaswdr/faker/v2 v2.3.0 github.com/mattn/go-sqlite3 v1.14.16 + github.com/rs/cors v1.11.0 + github.com/spf13/cobra v1.8.1 github.com/stephenafamo/bob v0.28.1 + golang.org/x/net v0.23.0 google.golang.org/protobuf v1.34.2 ) @@ -17,7 +21,9 @@ require ( github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/google/cel-go v0.20.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/stephenafamo/scan v0.4.2 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect diff --git a/go.sum b/go.sum index 8d26e64..c625d58 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-2024071716455 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240717164558-a6c49f84cc0f.2/go.mod h1:ylS4c28ACSI59oJrOdW4pHS4n0Hw4TgSPHn8rpHl4Yw= connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= +connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ= +connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= @@ -18,6 +20,7 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/bufbuild/protovalidate-go v0.6.3 h1:wxQyzW035zM16Binbaz/nWAzS12dRIXhZdSUWRY7Fv0= github.com/bufbuild/protovalidate-go v0.6.3/go.mod h1:J4PtwP9Z2YAGgB0+o+tTWEDtLtXvz/gfhFZD8pbzM/U= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= @@ -37,6 +40,8 @@ github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jaswdr/faker/v2 v2.3.0 h1:jgQ9UmU2Eb5tSQ8JkUS4tPoyTM2OtThQpOpwk7Fa9RY= github.com/jaswdr/faker/v2 v2.3.0/go.mod h1:ROK8xwQV0hYOLDUtxCQgHGcl10jbVzIvqHxcIDdwY2Q= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= @@ -63,10 +68,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc= github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stephenafamo/bob v0.28.1 h1:yQaHuhP9HoCldoRIrhB7SwcHKMCCSw5499h7ETFcBLs= github.com/stephenafamo/bob v0.28.1/go.mod h1:S/D3dAbBZjBOcts9iv4ywsJDApFK86s3bUBulCoT6kE= github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97 h1:XItoZNmhOih06TC02jK7l3wlpZ0XT/sPQYutDcGOQjg= @@ -90,8 +102,8 @@ github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwv github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA= github.com/volatiletech/strmangle v0.0.6 h1:AdOYE3B2ygRDq4rXDij/MMwq6KVK/pWAYxpC7CLrkKQ= github.com/volatiletech/strmangle v0.0.6/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= diff --git a/go/api/api.go b/go/api/api.go index 2d3f9c9..59440d6 100644 --- a/go/api/api.go +++ b/go/api/api.go @@ -8,7 +8,7 @@ import ( type API struct { mu sync.Mutex - db bob.Executor + DB bob.Executor } func (api *API) lockf(f func()) { diff --git a/go/api/devices_create.go b/go/api/devices_create.go index 16f5f72..2e873b0 100644 --- a/go/api/devices_create.go +++ b/go/api/devices_create.go @@ -15,7 +15,7 @@ import ( func (api *API) DevicesCreate(ctx context.Context, params *models.Device) (device *models.Device, err error) { now := time.Now() api.lockf(func() { - device, err = models.Devices.Insert(ctx, api.db, &models.DeviceSetter{ + device, err = models.Devices.Insert(ctx, api.DB, &models.DeviceSetter{ Slug: omit.From(params.Slug), Name: omit.From(params.Name), ResolutionX: omit.From(params.ResolutionX), diff --git a/go/api/devices_get_by_slug.go b/go/api/devices_get_by_slug.go index 08e2c6b..8b11410 100644 --- a/go/api/devices_get_by_slug.go +++ b/go/api/devices_get_by_slug.go @@ -9,7 +9,7 @@ import ( ) func (api *API) GetDevice(ctx context.Context, slug string) (device *models.Device, err error) { - device, err = models.FindDevice(ctx, api.db, slug) + device, err = models.FindDevice(ctx, api.DB, slug) if err != nil { if err.Error() == "sql: no rows in result set" { return device, errs.Wrapw(err, "device not found", "slug", slug).Code(connect.CodeNotFound) diff --git a/go/cmd/bluemage/main.go b/go/cmd/bluemage/main.go index 332d13d..7514539 100644 --- a/go/cmd/bluemage/main.go +++ b/go/cmd/bluemage/main.go @@ -1,7 +1,28 @@ package main -import "time" +import ( + "context" + "os" + "os/signal" + + "github.com/spf13/cobra" + "github.com/tigorlazuardi/bluemage/go/cmd/bluemage/serve" +) + +var Cmd = &cobra.Command{ + Use: "bluemage", + Short: "Bluemage is a reddit image downloader", +} + +func init() { + Cmd.AddCommand(serve.Cmd) +} func main() { - time.Now().Unix() + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := Cmd.ExecuteContext(ctx); err != nil { + os.Exit(1) + } } diff --git a/go/cmd/bluemage/serve/serve.go b/go/cmd/bluemage/serve/serve.go new file mode 100644 index 0000000..c9d16f0 --- /dev/null +++ b/go/cmd/bluemage/serve/serve.go @@ -0,0 +1,63 @@ +package serve + +import ( + "context" + "errors" + "log/slog" + "net/http" + "time" + + "connectrpc.com/connect" + "github.com/spf13/cobra" + "github.com/stephenafamo/bob" + "github.com/tigorlazuardi/bluemage/go/api" + "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1/v1connect" + "github.com/tigorlazuardi/bluemage/go/pkg/errs" + "github.com/tigorlazuardi/bluemage/go/server" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +var Cmd = &cobra.Command{ + Use: "serve", + RunE: func(cmd *cobra.Command, args []string) error { + db, err := bob.Open("sqlite3", "file:go/data.db") + if err != nil { + return errs.Wrap(err, "failed to open database") + } + + api := &api.API{ + DB: db, + } + + handler := &server.Server{ + DeviceHandler: server.DeviceHandler{ + API: api, + }, + } + + mux := http.NewServeMux() + mux.Handle(v1connect.NewDeviceServiceHandler(handler, connect.WithInterceptors(server.LogInterceptor()))) + + server := &http.Server{ + Addr: ":8080", + Handler: h2c.NewHandler(server.WithCORS(mux), &http2.Server{}), + } + + go func() { + <-cmd.Context().Done() + slog.Info("Exit signal received. Shutting down server") + shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + _ = server.Shutdown(shutdownCtx) + }() + + slog.Info("ConnectRPC server started", "addr", server.Addr) + err = server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return errs.Wrap(err, "failed to serve") + } + return errors.Join(db.Close()) + }, + SilenceUsage: true, +} diff --git a/go/pkg/errs/errs.go b/go/pkg/errs/errs.go index 5351485..4e38626 100644 --- a/go/pkg/errs/errs.go +++ b/go/pkg/errs/errs.go @@ -229,3 +229,11 @@ func FindError(err error) Error { return nil } } + +func DrillToError(err error) error { + e := FindError(err) + if e != nil { + return e + } + return err +} diff --git a/go/server/cors.go b/go/server/cors.go new file mode 100644 index 0000000..63175bf --- /dev/null +++ b/go/server/cors.go @@ -0,0 +1,21 @@ +package server + +import ( + "net/http" + + connectcors "connectrpc.com/cors" + "github.com/rs/cors" +) + +// WithCORS adds CORS support to a Connect HTTP handler. +func WithCORS(h http.Handler) http.Handler { + middleware := cors.New(cors.Options{ + // TODO: AllowedOrigins will need to be limited when + // the client is embedded to the binary. + AllowedOrigins: []string{"*"}, + AllowedMethods: connectcors.AllowedMethods(), + AllowedHeaders: connectcors.AllowedHeaders(), + ExposedHeaders: connectcors.ExposedHeaders(), + }) + return middleware.Handler(h) +} diff --git a/go/server/interceptor.go b/go/server/interceptor.go new file mode 100644 index 0000000..193848a --- /dev/null +++ b/go/server/interceptor.go @@ -0,0 +1,39 @@ +package server + +import ( + "context" + "fmt" + "log/slog" + "time" + + "connectrpc.com/connect" + "github.com/tigorlazuardi/bluemage/go/pkg/errs" +) + +func LogInterceptor() connect.UnaryInterceptorFunc { + interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) { + start := time.Now() + resp, err := next(ctx, ar) + dur := time.Since(start) + millisecondsFloat := float64(dur) / float64(time.Millisecond) + if err != nil { + slog.Error("RPC Error", + "procedure", ar.Spec().Procedure, + "method", ar.HTTPMethod(), + "duration", fmt.Sprintf("%.3fs", millisecondsFloat), + "error", errs.DrillToError(err), + ) + } else { + slog.Info("RPC Call", + "procedure", ar.Spec().Procedure, + "method", ar.HTTPMethod(), + "duration", fmt.Sprintf("%.3fs", millisecondsFloat), + ) + } + + return resp, err + } + } + return connect.UnaryInterceptorFunc(interceptor) +} diff --git a/schemas/proto/buf.gen.web.yaml b/schemas/proto/buf.gen.web.yaml index 51e6ea4..432ae00 100644 --- a/schemas/proto/buf.gen.web.yaml +++ b/schemas/proto/buf.gen.web.yaml @@ -1,8 +1,8 @@ version: v2 plugins: - local: protoc-gen-es - out: ../../web/gen/proto + out: ../../web/src/gen/proto opt: target=ts - local: protoc-gen-connect-es - out: ../../web/gen/proto + out: ../../web/src/gen/proto opt: target=ts diff --git a/web/.env b/web/.env new file mode 100644 index 0000000..ccb1bdd --- /dev/null +++ b/web/.env @@ -0,0 +1 @@ +VITE_BLUEMAGE_API_URL=http://localhost:8080 diff --git a/web/src/App.svelte b/web/src/App.svelte index e8b590f..21bbedc 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -2,6 +2,13 @@ import svelteLogo from './assets/svelte.svg' import viteLogo from '/vite.svg' import Counter from './lib/Counter.svelte' + import { client } from './client/client'; + + function getDevice() { + return client.getDevice({slug: 'test'}) + } + + let promise = getDevice()
@@ -26,6 +33,14 @@

Click on the Vite and Svelte logos to learn more

+ + {#await promise} +

loading...

+ {:then device} +

{device.name}

+ {:catch error} +

{error.message}

+ {/await}