| @ -0,0 +1,117 @@ | |||||
| # Created by https://www.toptal.com/developers/gitignore/api/goland+all,go | |||||
| # Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,go | |||||
| ### Go ### | |||||
| # If you prefer the allow list template instead of the deny list, see community template: | |||||
| # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore | |||||
| # | |||||
| # Binaries for programs and plugins | |||||
| *.exe | |||||
| *.exe~ | |||||
| *.dll | |||||
| *.so | |||||
| *.dylib | |||||
| # Test binary, built with `go test -c` | |||||
| *.test | |||||
| # Output of the go coverage tool, specifically when used with LiteIDE | |||||
| *.out | |||||
| # Dependency directories (remove the comment below to include it) | |||||
| # vendor/ | |||||
| # Go workspace file | |||||
| go.work | |||||
| ### GoLand+all ### | |||||
| # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider | |||||
| # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 | |||||
| # User-specific stuff | |||||
| .idea/**/workspace.xml | |||||
| .idea/**/tasks.xml | |||||
| .idea/**/usage.statistics.xml | |||||
| .idea/**/dictionaries | |||||
| .idea/**/shelf | |||||
| # AWS User-specific | |||||
| .idea/**/aws.xml | |||||
| # Generated files | |||||
| .idea/**/contentModel.xml | |||||
| # Sensitive or high-churn files | |||||
| .idea/**/dataSources/ | |||||
| .idea/**/dataSources.ids | |||||
| .idea/**/dataSources.local.xml | |||||
| .idea/**/sqlDataSources.xml | |||||
| .idea/**/dynamic.xml | |||||
| .idea/**/uiDesigner.xml | |||||
| .idea/**/dbnavigator.xml | |||||
| # Gradle | |||||
| .idea/**/gradle.xml | |||||
| .idea/**/libraries | |||||
| # Gradle and Maven with auto-import | |||||
| # When using Gradle or Maven with auto-import, you should exclude module files, | |||||
| # since they will be recreated, and may cause churn. Uncomment if using | |||||
| # auto-import. | |||||
| # .idea/artifacts | |||||
| # .idea/compiler.xml | |||||
| # .idea/jarRepositories.xml | |||||
| # .idea/modules.xml | |||||
| # .idea/*.iml | |||||
| # .idea/modules | |||||
| # *.iml | |||||
| # *.ipr | |||||
| # CMake | |||||
| cmake-build-*/ | |||||
| # Mongo Explorer plugin | |||||
| .idea/**/mongoSettings.xml | |||||
| # File-based project format | |||||
| *.iws | |||||
| # IntelliJ | |||||
| out/ | |||||
| # mpeltonen/sbt-idea plugin | |||||
| .idea_modules/ | |||||
| # JIRA plugin | |||||
| atlassian-ide-plugin.xml | |||||
| # Cursive Clojure plugin | |||||
| .idea/replstate.xml | |||||
| # SonarLint plugin | |||||
| .idea/sonarlint/ | |||||
| # Crashlytics plugin (for Android Studio and IntelliJ) | |||||
| com_crashlytics_export_strings.xml | |||||
| crashlytics.properties | |||||
| crashlytics-build.properties | |||||
| fabric.properties | |||||
| # Editor-based Rest Client | |||||
| .idea/httpRequests | |||||
| # Android studio 3.1+ serialized cache file | |||||
| .idea/caches/build_file_checksums.ser | |||||
| ### GoLand+all Patch ### | |||||
| # Ignore everything but code style settings and run configurations | |||||
| # that are supposed to be shared within teams. | |||||
| .idea/* | |||||
| !.idea/codeStyles | |||||
| !.idea/runConfigurations | |||||
| # End of https://www.toptal.com/developers/gitignore/api/goland+all,go | |||||
| build | |||||
| @ -0,0 +1,68 @@ | |||||
| package main | |||||
| import ( | |||||
| "flag" | |||||
| "log" | |||||
| "git.brettb.xyz/goinv/server/internal/api" | |||||
| "git.brettb.xyz/goinv/server/internal/storage" | |||||
| "git.brettb.xyz/goinv/server/internal/types" | |||||
| ) | |||||
| func main() { | |||||
| seed := flag.Bool("seed", false, "seed the database") | |||||
| flag.Parse() | |||||
| datastore, err := storage.NewDataStorePG("127.0.0.1", "postgres", "password", "em_test", "disable") // TODO: CONFIGURATION | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| if *seed { | |||||
| log.Println("Seeding database") | |||||
| cat := types.Category{ | |||||
| Name: "House", | |||||
| } | |||||
| if err := datastore.CreateCategory(&cat); err != nil { | |||||
| panic(err) | |||||
| } | |||||
| building := types.Building{ | |||||
| Name: "Memorial Student Center", | |||||
| } | |||||
| if err := datastore.CreateBuilding(&building); err != nil { | |||||
| panic(err) | |||||
| } | |||||
| shelf := types.ShelfLocation{ | |||||
| Name: "SHELF-TEST", | |||||
| BuildingID: building.ID, | |||||
| } | |||||
| if err := datastore.CreateShelfLocation(&shelf); err != nil { | |||||
| panic(err) | |||||
| } | |||||
| if err := datastore.CreateAsset(&types.Asset{ | |||||
| Name: "Test", | |||||
| Quantity: 1, | |||||
| Length: "6 in", | |||||
| Manufacturer: "Testing", | |||||
| ModelName: "Test", | |||||
| Price: 420.69, | |||||
| Comments: "", | |||||
| ShelfLocationID: &shelf.ID, | |||||
| CategoryID: &cat.ID, | |||||
| }); err != nil { | |||||
| panic(err) | |||||
| } | |||||
| return | |||||
| } | |||||
| s := api.NewAPIServer(datastore, ":3001") | |||||
| s.Run() | |||||
| } | |||||
| @ -0,0 +1,20 @@ | |||||
| module git.brettb.xyz/goinv/server | |||||
| go 1.21.6 | |||||
| require gorm.io/driver/postgres v1.5.4 | |||||
| require github.com/ajg/form v1.5.1 // indirect | |||||
| require ( | |||||
| github.com/go-chi/chi/v5 v5.0.11 | |||||
| github.com/go-chi/render v1.0.3 | |||||
| github.com/jackc/pgpassfile v1.0.0 // indirect | |||||
| github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect | |||||
| github.com/jackc/pgx/v5 v5.4.3 // indirect | |||||
| github.com/jinzhu/inflection v1.0.0 // indirect | |||||
| github.com/jinzhu/now v1.1.5 // indirect | |||||
| golang.org/x/crypto v0.14.0 // indirect | |||||
| golang.org/x/text v0.13.0 // indirect | |||||
| gorm.io/gorm v1.25.5 | |||||
| ) | |||||
| @ -0,0 +1,38 @@ | |||||
| github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= | |||||
| github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= | |||||
| 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/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= | |||||
| github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= | |||||
| github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= | |||||
| github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= | |||||
| github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= | |||||
| github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= | |||||
| github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= | |||||
| github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= | |||||
| github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= | |||||
| github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= | |||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | |||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | |||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | |||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | |||||
| 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/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | |||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | |||||
| github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | |||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | |||||
| golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= | |||||
| golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= | |||||
| golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= | |||||
| golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | |||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |||||
| gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= | |||||
| gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= | |||||
| gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= | |||||
| gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= | |||||
| @ -0,0 +1,77 @@ | |||||
| package api | |||||
| import ( | |||||
| "encoding/json" | |||||
| "log" | |||||
| "net/http" | |||||
| "git.brettb.xyz/goinv/server/internal/storage" | |||||
| "git.brettb.xyz/goinv/server/internal/types" | |||||
| "github.com/go-chi/chi/v5" | |||||
| "github.com/go-chi/chi/v5/middleware" | |||||
| "github.com/go-chi/render" | |||||
| ) | |||||
| const API_VERSION_MAJOR = 0 | |||||
| const API_VERSION_MINOR = 0 | |||||
| const API_VERSION_PATCH = 1 | |||||
| var API_VERSION = types.APIVersion{ | |||||
| Major: API_VERSION_MAJOR, | |||||
| Minor: API_VERSION_MINOR, | |||||
| Patch: API_VERSION_PATCH, | |||||
| } | |||||
| type APIFunc func(w http.ResponseWriter, r *http.Request) error | |||||
| type APIServer struct { | |||||
| listenAddr string | |||||
| db *storage.DataStore | |||||
| } | |||||
| func NewAPIServer(database *storage.DataStore, listenAddr string) *APIServer { | |||||
| return &APIServer{ | |||||
| listenAddr: listenAddr, | |||||
| db: database, | |||||
| } | |||||
| } | |||||
| func (s *APIServer) Run() { | |||||
| r := chi.NewRouter() | |||||
| r.Use(middleware.Logger) | |||||
| r.Use(render.SetContentType(render.ContentTypeJSON)) | |||||
| s.registerRoutes(r) | |||||
| log.Printf("API Server listening on %s", s.listenAddr) | |||||
| err := http.ListenAndServe(s.listenAddr, r) | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| } | |||||
| func (s *APIServer) registerRoutes(r *chi.Mux) { | |||||
| r.Get("/", makeHandler(s.handleIndex)) | |||||
| r.Route("/assets", s.setupAssetRoutes()) | |||||
| r.Route("/shelves", s.setupShelfRoutes()) | |||||
| } | |||||
| func (s *APIServer) handleIndex(w http.ResponseWriter, r *http.Request) error { | |||||
| return writeJSON(w, http.StatusOK, types.IndexResponse{Version: API_VERSION}) | |||||
| } | |||||
| func makeHandler(f APIFunc) http.HandlerFunc { | |||||
| return func(w http.ResponseWriter, r *http.Request) { | |||||
| if err := f(w, r); err != nil { | |||||
| render.Render(w, r, errBadRequest(err)) | |||||
| } | |||||
| } | |||||
| } | |||||
| func writeJSON(w http.ResponseWriter, s int, v any) error { | |||||
| w.WriteHeader(s) | |||||
| return json.NewEncoder(w).Encode(v) | |||||
| } | |||||
| @ -0,0 +1,124 @@ | |||||
| package api | |||||
| import ( | |||||
| "context" | |||||
| "log" | |||||
| "net/http" | |||||
| "strconv" | |||||
| "git.brettb.xyz/goinv/server/internal/types" | |||||
| "github.com/go-chi/chi/v5" | |||||
| "github.com/go-chi/render" | |||||
| ) | |||||
| func (s *APIServer) setupAssetRoutes() func(r chi.Router) { | |||||
| return func(r chi.Router) { | |||||
| r.Get("/", makeHandler(s.getAssets)) | |||||
| r.Post("/", makeHandler(s.createAsset)) | |||||
| r.Route("/{assetID}", func(r chi.Router) { | |||||
| r.Use(s.AssetCtx) | |||||
| r.Get("/", makeHandler(s.getAsset)) | |||||
| r.Delete("/", makeHandler(s.deleteAsset)) | |||||
| }) | |||||
| } | |||||
| } | |||||
| func (s *APIServer) AssetCtx(next http.Handler) http.Handler { | |||||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||||
| assetIdStr := chi.URLParam(r, "assetID") | |||||
| assetId, err := strconv.Atoi(assetIdStr) | |||||
| if err != nil { | |||||
| render.Render(w, r, errNotFound) | |||||
| return | |||||
| } | |||||
| asset, err := s.db.GetAssetByID(uint64(assetId)) | |||||
| if err != nil { | |||||
| render.Render(w, r, errNotFound) | |||||
| return | |||||
| } | |||||
| ctx := context.WithValue(r.Context(), "asset", asset) | |||||
| next.ServeHTTP(w, r.WithContext(ctx)) | |||||
| }) | |||||
| } | |||||
| func (s *APIServer) getAssets(w http.ResponseWriter, r *http.Request) error { | |||||
| assets, err := s.db.GetAssets(0, 50) // TODO: Proper Pagination | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| total, err := s.db.TotalAssets() | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return render.Render(w, r, &types.MultipleAssetsResponse{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusOK, | |||||
| }, | |||||
| Assets: assets, | |||||
| Total: total, | |||||
| }) | |||||
| } | |||||
| func (s *APIServer) getAsset(w http.ResponseWriter, r *http.Request) error { | |||||
| ctx := r.Context() | |||||
| asset, ok := ctx.Value("asset").(*types.Asset) | |||||
| if !ok { | |||||
| return render.Render(w, r, errUnprocessable) | |||||
| } | |||||
| return render.Render(w, r, &types.AssetResponse{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusOK, | |||||
| }, | |||||
| Asset: asset, | |||||
| }) | |||||
| } | |||||
| func (s *APIServer) deleteAsset(w http.ResponseWriter, r *http.Request) error { | |||||
| ctx := r.Context() | |||||
| asset, ok := ctx.Value("asset").(*types.Asset) | |||||
| if !ok { | |||||
| return render.Render(w, r, errUnprocessable) | |||||
| } | |||||
| if ok, _ := s.db.DeleteAssetByID(asset.ID); !ok { | |||||
| return render.Render(w, r, errUnprocessable) | |||||
| } | |||||
| return render.Render(w, r, &types.AssetResponse{Response: &types.Response{HTTPStatusCode: http.StatusOK}, Asset: asset}) | |||||
| } | |||||
| func (s *APIServer) createAsset(w http.ResponseWriter, r *http.Request) error { | |||||
| data := &types.CreateAssetRequest{} | |||||
| if err := render.Bind(r, data); err != nil { | |||||
| log.Printf("ERR: %v\n", err) | |||||
| return render.Render(w, r, errBadRequest(err)) | |||||
| } | |||||
| asset := &types.Asset{ | |||||
| Name: data.Name, | |||||
| Quantity: data.Quantity, | |||||
| Length: data.Length, | |||||
| Manufacturer: data.Manufacturer, | |||||
| ModelName: data.ModelName, | |||||
| Price: data.Price, | |||||
| Comments: data.Comments, | |||||
| ShelfLocationID: data.ShelfLocationID, | |||||
| CategoryID: data.CategoryID, | |||||
| } | |||||
| err := s.db.CreateAsset(asset) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return render.Render(w, r, &types.AssetResponse{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusOK, | |||||
| }, | |||||
| Asset: asset, | |||||
| }) | |||||
| } | |||||
| @ -0,0 +1,96 @@ | |||||
| package api | |||||
| import ( | |||||
| "context" | |||||
| "net/http" | |||||
| "strconv" | |||||
| "git.brettb.xyz/goinv/server/internal/types" | |||||
| "github.com/go-chi/chi/v5" | |||||
| "github.com/go-chi/render" | |||||
| ) | |||||
| func (s *APIServer) setupShelfRoutes() func(chi.Router) { | |||||
| return func(r chi.Router) { | |||||
| r.Get("/", makeHandler(s.getShelves)) | |||||
| //r.Post("/", makeHandler(s.createShelf)) | |||||
| r.Route("/{shelfID}", func(r chi.Router) { | |||||
| r.Use(s.ShelfCtx) | |||||
| r.Get("/", makeHandler(s.getShelf)) | |||||
| r.Delete("/", makeHandler(s.deleteShelf)) | |||||
| }) | |||||
| } | |||||
| } | |||||
| func (s *APIServer) ShelfCtx(next http.Handler) http.Handler { | |||||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||||
| shelfIdStr := chi.URLParam(r, "shelfID") | |||||
| shelfId, err := strconv.ParseUint(shelfIdStr, 10, 64) | |||||
| if err != nil { | |||||
| render.Render(w, r, errNotFound) | |||||
| return | |||||
| } | |||||
| shelf, err := s.db.GetShelfByID(shelfId) | |||||
| if err != nil { | |||||
| render.Render(w, r, errNotFound) | |||||
| return | |||||
| } | |||||
| ctx := context.WithValue(r.Context(), "shelf", shelf) | |||||
| next.ServeHTTP(w, r.WithContext(ctx)) | |||||
| }) | |||||
| } | |||||
| func (s *APIServer) getShelves(w http.ResponseWriter, r *http.Request) error { | |||||
| shelves, err := s.db.GetShelves(0, 10) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| total, err := s.db.TotalShelves() | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return render.Render(w, r, &types.MultipleShelfResponse{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusOK, | |||||
| }, | |||||
| ShelfLocations: shelves, | |||||
| Total: total, | |||||
| }) | |||||
| } | |||||
| func (s *APIServer) getShelf(w http.ResponseWriter, r *http.Request) error { | |||||
| ctx := r.Context() | |||||
| shelf, ok := ctx.Value("shelf").(*types.ShelfLocation) | |||||
| if !ok { | |||||
| return render.Render(w, r, errUnprocessable) | |||||
| } | |||||
| return render.Render(w, r, &types.ShelfResponse{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusOK, | |||||
| }, | |||||
| ShelfLocation: shelf, | |||||
| }) | |||||
| } | |||||
| func (s *APIServer) deleteShelf(w http.ResponseWriter, r *http.Request) error { | |||||
| ctx := r.Context() | |||||
| shelf, ok := ctx.Value("shelf").(*types.ShelfLocation) | |||||
| if !ok { | |||||
| return render.Render(w, r, errUnprocessable) | |||||
| } | |||||
| if ok, _ := s.db.DeleteShelfByID(shelf.ID); !ok { | |||||
| return render.Render(w, r, errUnprocessable) | |||||
| } | |||||
| return render.Render(w, r, &types.ShelfResponse{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusOK, | |||||
| }, | |||||
| ShelfLocation: shelf, | |||||
| }) | |||||
| } | |||||
| @ -0,0 +1,52 @@ | |||||
| package api | |||||
| import ( | |||||
| "net/http" | |||||
| "git.brettb.xyz/goinv/server/internal/types" | |||||
| "github.com/go-chi/render" | |||||
| ) | |||||
| var errNotFound = &types.APIError{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusNotFound, | |||||
| }, | |||||
| Messages: []string{"resource not found"}, | |||||
| } | |||||
| var errUnprocessable = &types.APIError{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusUnprocessableEntity, | |||||
| }, | |||||
| Messages: []string{"unable to process"}, | |||||
| } | |||||
| func errBadRequest(err error) render.Renderer { | |||||
| return &types.APIError{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusBadRequest, | |||||
| }, | |||||
| Err: err, | |||||
| Messages: []string{"bad request"}, | |||||
| } | |||||
| } | |||||
| func errRender(err error) render.Renderer { | |||||
| return &types.APIError{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusUnprocessableEntity, | |||||
| }, | |||||
| Err: err, | |||||
| Messages: []string{"error rendering response"}, | |||||
| } | |||||
| } | |||||
| func errUnauthorized(err error) render.Renderer { | |||||
| return &types.APIError{ | |||||
| Response: &types.Response{ | |||||
| HTTPStatusCode: http.StatusUnauthorized, | |||||
| }, | |||||
| Err: err, | |||||
| Messages: []string{"unauthorized"}, | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,131 @@ | |||||
| package storage | |||||
| import ( | |||||
| "fmt" | |||||
| "log" | |||||
| "git.brettb.xyz/goinv/server/internal/types" | |||||
| "gorm.io/driver/postgres" | |||||
| "gorm.io/gorm" | |||||
| "gorm.io/gorm/logger" | |||||
| ) | |||||
| type DataStore struct { | |||||
| db *gorm.DB | |||||
| } | |||||
| func NewDataStorePG(host, user, password, dbname, sslmode string) (*DataStore, error) { | |||||
| dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=%s", host, user, password, dbname, sslmode) | |||||
| log.Printf("Connecting to %s@%s/%s\n", host, user, dbname) | |||||
| db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ | |||||
| Logger: logger.Default.LogMode(logger.Silent), | |||||
| }) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| log.Printf("Auto-Migrating models") | |||||
| if err := db.AutoMigrate( | |||||
| &types.Asset{}, | |||||
| &types.Building{}, | |||||
| &types.Category{}, | |||||
| &types.ShelfLocation{}, | |||||
| ); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return &DataStore{ | |||||
| db: db, | |||||
| }, nil | |||||
| } | |||||
| func (s *DataStore) CreateAsset(asset *types.Asset) error { | |||||
| result := s.db.Create(asset) | |||||
| return result.Error | |||||
| } | |||||
| func (s *DataStore) CreateBuilding(building *types.Building) error { | |||||
| result := s.db.Create(building) | |||||
| return result.Error | |||||
| } | |||||
| func (s *DataStore) CreateCategory(category *types.Category) error { | |||||
| result := s.db.Create(category) | |||||
| return result.Error | |||||
| } | |||||
| func (s *DataStore) CreateShelfLocation(shelf *types.ShelfLocation) error { | |||||
| result := s.db.Create(shelf) | |||||
| return result.Error | |||||
| } | |||||
| func (s *DataStore) GetAssetByID(id uint64) (*types.Asset, error) { | |||||
| var result types.Asset | |||||
| tx := s.db.Model(&types.Asset{}).Where("id = ?", id).First(&result) | |||||
| if tx.Error != nil { | |||||
| return nil, fmt.Errorf("asset %d not found", id) | |||||
| } | |||||
| return &result, nil | |||||
| } | |||||
| func (s *DataStore) GetAssets(offset, limit uint64) ([]*types.Asset, error) { | |||||
| var assets []*types.Asset | |||||
| s.db.Joins("Category").Joins("ShelfLocation").Order("id asc").Offset(int(offset)).Limit(int(limit)).Find(&assets) | |||||
| if len(assets) == 0 { | |||||
| return nil, fmt.Errorf("no assets found") | |||||
| } | |||||
| return assets, nil | |||||
| } | |||||
| func (s *DataStore) TotalAssets() (int64, error) { | |||||
| var count int64 | |||||
| if tx := s.db.Find(&types.Asset{}).Count(&count); tx.Error != nil { | |||||
| return 0, tx.Error | |||||
| } | |||||
| return count, nil | |||||
| } | |||||
| func (s *DataStore) DeleteAssetByID(id uint64) (bool, error) { | |||||
| tx := s.db.Delete(&types.Asset{}, id) | |||||
| if tx.Error != nil { | |||||
| return false, fmt.Errorf("unable to delete: %s", tx.Error.Error()) | |||||
| } | |||||
| return true, nil | |||||
| } | |||||
| func (s *DataStore) GetShelfByID(id uint64) (*types.ShelfLocation, error) { | |||||
| var result types.ShelfLocation | |||||
| tx := s.db.Model(&types.ShelfLocation{}).Where("id = ?", id).First(&result) | |||||
| if tx.Error != nil { | |||||
| return nil, fmt.Errorf("shelf %d not found", id) | |||||
| } | |||||
| return &result, nil | |||||
| } | |||||
| func (s *DataStore) GetShelves(offset, limit int) ([]*types.ShelfLocation, error) { | |||||
| var shelves []*types.ShelfLocation | |||||
| s.db.Offset(offset).Limit(limit).Find(&shelves) | |||||
| if len(shelves) == 0 { | |||||
| return nil, fmt.Errorf("no shelves found") | |||||
| } | |||||
| return shelves, nil | |||||
| } | |||||
| func (s *DataStore) TotalShelves() (int64, error) { | |||||
| var count int64 | |||||
| if tx := s.db.Find(&types.ShelfLocation{}).Count(&count); tx.Error != nil { | |||||
| return 0, tx.Error | |||||
| } | |||||
| return count, nil | |||||
| } | |||||
| func (s *DataStore) DeleteShelfByID(id uint64) (bool, error) { | |||||
| tx := s.db.Delete(&types.ShelfLocation{}, id) | |||||
| if tx.Error != nil { | |||||
| return false, fmt.Errorf("unable to delete: %s", tx.Error.Error()) | |||||
| } | |||||
| return true, nil | |||||
| } | |||||
| @ -0,0 +1,44 @@ | |||||
| package types | |||||
| import ( | |||||
| "net/http" | |||||
| "github.com/go-chi/render" | |||||
| ) | |||||
| type Response struct { | |||||
| HTTPStatusCode int `json:"status"` | |||||
| } | |||||
| func (r *Response) Render(w http.ResponseWriter, req *http.Request) error { | |||||
| render.Status(req, r.HTTPStatusCode) | |||||
| return nil | |||||
| } | |||||
| type APIVersion struct { | |||||
| Major int `json:"major"` | |||||
| Minor int `json:"minor"` | |||||
| Patch int `json:"patch"` | |||||
| } | |||||
| type IndexResponse struct { | |||||
| *Response | |||||
| Version APIVersion `json:"version"` | |||||
| } | |||||
| type APIError struct { | |||||
| *Response | |||||
| Err error `json:"-"` | |||||
| Messages []string `json:"messages"` | |||||
| } | |||||
| func NewAPIError(status int, messages ...string) *APIError { | |||||
| return &APIError{ | |||||
| Response: &Response{ | |||||
| HTTPStatusCode: status, | |||||
| }, | |||||
| Err: nil, | |||||
| Messages: messages, | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,63 @@ | |||||
| package types | |||||
| import ( | |||||
| "net/http" | |||||
| "time" | |||||
| "gorm.io/gorm" | |||||
| ) | |||||
| /* | |||||
| Base Model | |||||
| */ | |||||
| type Asset struct { | |||||
| ID uint64 `gorm:"primarykey" json:"id"` | |||||
| Name string `json:"name"` | |||||
| Quantity int `json:"quantity"` | |||||
| Length string `json:"length,omitempty"` | |||||
| Manufacturer string `json:"manufacturer,omitempty"` | |||||
| ModelName string `json:"model_name,omitempty"` | |||||
| Price float64 `json:"price,omitempty"` | |||||
| Comments string `json:"comments,omitempty"` | |||||
| ShelfLocationID *uint64 `json:"-"` | |||||
| ShelfLocation *ShelfLocation `json:"shelf_location,omitempty"` | |||||
| CategoryID *uint64 `json:"-"` | |||||
| Category *Category `json:"category,omitempty"` | |||||
| CreatedAt time.Time `json:"created_at"` | |||||
| UpdatedAt time.Time `json:"updated_at"` | |||||
| DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` | |||||
| } | |||||
| /* | |||||
| Requests | |||||
| */ | |||||
| type CreateAssetRequest struct { | |||||
| Name string `json:"name"` | |||||
| Quantity int `json:"quantity"` | |||||
| Length string `json:"length,omitempty"` | |||||
| Manufacturer string `json:"manufacturer,omitempty"` | |||||
| ModelName string `json:"model_name,omitempty"` | |||||
| Price float64 `json:"price,omitempty"` | |||||
| Comments string `json:"comments,omitempty"` | |||||
| ShelfLocationID *uint64 `json:"shelf_location_id,omitempty"` | |||||
| CategoryID *uint64 `json:"category_id,omitempty"` | |||||
| } | |||||
| func (c CreateAssetRequest) Bind(r *http.Request) error { return nil } | |||||
| /* | |||||
| Responses | |||||
| */ | |||||
| type AssetResponse struct { | |||||
| *Response | |||||
| Asset *Asset `json:"asset"` | |||||
| } | |||||
| type MultipleAssetsResponse struct { | |||||
| *Response | |||||
| Assets []*Asset `json:"assets"` | |||||
| Total int64 `json:"total"` | |||||
| } | |||||
| @ -0,0 +1,28 @@ | |||||
| package types | |||||
| import ( | |||||
| "time" | |||||
| "gorm.io/gorm" | |||||
| ) | |||||
| /* | |||||
| Base Model | |||||
| */ | |||||
| type Building struct { | |||||
| ID uint64 `gorm:"primarykey" json:"id"` | |||||
| Name string `json:"name"` | |||||
| ShelfLocations []ShelfLocation `json:"shelves"` | |||||
| CreatedAt time.Time `json:"created_at"` | |||||
| UpdatedAt time.Time `json:"updated_at"` | |||||
| DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` | |||||
| } | |||||
| /* | |||||
| Requests | |||||
| */ | |||||
| /* | |||||
| Responses | |||||
| */ | |||||
| @ -0,0 +1,28 @@ | |||||
| package types | |||||
| import ( | |||||
| "time" | |||||
| "gorm.io/gorm" | |||||
| ) | |||||
| /* | |||||
| Base Model | |||||
| */ | |||||
| type Category struct { | |||||
| ID uint64 `gorm:"primarykey" json:"id"` | |||||
| Name string `json:"name"` | |||||
| Assets []Asset `json:"assets,omitempty"` | |||||
| CreatedAt time.Time `json:"created_at"` | |||||
| UpdatedAt time.Time `json:"updated_at"` | |||||
| DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` | |||||
| } | |||||
| /* | |||||
| Requests | |||||
| */ | |||||
| /* | |||||
| Responses | |||||
| */ | |||||
| @ -0,0 +1,43 @@ | |||||
| package types | |||||
| import ( | |||||
| "time" | |||||
| "gorm.io/gorm" | |||||
| ) | |||||
| /* | |||||
| Base Model | |||||
| */ | |||||
| type ShelfLocation struct { | |||||
| ID uint64 `gorm:"primarykey" json:"id"` | |||||
| Name string `json:"name"` | |||||
| RoomNumber string `json:"room_number,omitempty"` | |||||
| Description string `json:"description,omitempty"` | |||||
| BuildingID uint64 `json:"-"` | |||||
| Building Building `json:"building"` | |||||
| Assets []Asset `json:"assets,omitempty"` | |||||
| CreatedAt time.Time `json:"created_at"` | |||||
| UpdatedAt time.Time `json:"updated_at"` | |||||
| DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` | |||||
| } | |||||
| /* | |||||
| Requests | |||||
| */ | |||||
| /* | |||||
| Responses | |||||
| */ | |||||
| type ShelfResponse struct { | |||||
| *Response | |||||
| ShelfLocation *ShelfLocation `json:"shelf"` | |||||
| } | |||||
| type MultipleShelfResponse struct { | |||||
| *Response | |||||
| ShelfLocations []*ShelfLocation `json:"shelves"` | |||||
| Total int64 `json:"total"` | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| //go:build mage | |||||
| // +build mage | |||||
| package main | |||||
| import ( | |||||
| "github.com/magefile/mage/mg" | |||||
| "github.com/magefile/mage/sh" | |||||
| ) | |||||
| var Default = Build | |||||
| func Build() error { | |||||
| if err := sh.Run("go", "mod", "download"); err != nil { | |||||
| return err | |||||
| } | |||||
| return sh.Run("go", "build", "-o", "./build/goinv-server", "./cmd/server") | |||||
| } | |||||
| func Run() error { | |||||
| mg.Deps(Build) | |||||
| return sh.RunV("./build/goinv-server") | |||||
| } | |||||