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