├── .gitattributes ├── .github └── workflows │ ├── deploy.yaml │ └── test.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── api.go ├── defaults.go ├── root.go └── version.go ├── embed ├── embed.go ├── postgres_migrations │ ├── 0001_init.down.sql │ └── 0001_init.up.sql └── public_html │ └── api-docs │ ├── index.html │ └── swagger.json ├── go.mod ├── go.sum ├── gorestapi ├── gorestapi.go ├── mainrpc │ ├── mainrpc.go │ ├── thing.go │ ├── thing_test.go │ ├── widget.go │ └── widget_test.go ├── swagger.go ├── thing.go ├── thing_test.go ├── widget.go └── widget_test.go ├── insomnia-collection.yaml ├── main.go ├── mocks ├── GRStore.go ├── ThingStore.go └── empty.go ├── store └── postgres │ ├── client.go │ ├── thing.go │ └── widget.go └── ui ├── .env ├── .eslintrc.js ├── .gitignore ├── README.md ├── index.html ├── package.json ├── prettier.config.js ├── public ├── favicon.ico └── manifest.json ├── src ├── App.tsx ├── authProvider.ts.disabled ├── dataOverrides.ts ├── dataProvider.ts ├── index.tsx ├── resources │ ├── Dashboard.tsx │ ├── Things.tsx │ └── Widgets.tsx └── vite-env.d.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | id-token: write 10 | contents: read 11 | 12 | env: 13 | REPO_NAME: docker.io/snowzach/gorestapi 14 | CGO_ENABLED: 0 15 | 16 | jobs: 17 | build: 18 | runs-on: [ubuntu-latest] 19 | steps: 20 | - name: Log in to Docker Hub 21 | uses: docker/login-action@v3 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | 26 | - uses: webfactory/ssh-agent@v0.8.0 27 | with: 28 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 29 | 30 | - uses: actions/checkout@v3 31 | 32 | - name: Setup Environment 33 | id: setup-environment 34 | run: | 35 | TAG_SHA_SHORT=$(git rev-parse --short HEAD) 36 | echo "FULL_REPO_NAME=$REPO_NAME" >> $GITHUB_ENV 37 | echo "TAG_SHA_SHORT=${TAG_SHA_SHORT}" >> $GITHUB_ENV 38 | 39 | EXTRA_TAGS="" # Add any extra tags you want to this variable (space delimited) 40 | 41 | # Version tag 42 | if [[ "${{ github.ref_name }}" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+$) ]]; then 43 | EXTRA_TAGS+="${BASH_REMATCH[1]} " 44 | fi 45 | 46 | echo "EXTRA_TAGS=$EXTRA_TAGS" >> $GITHUB_ENV 47 | 48 | - name: Build Image 49 | id: build-image 50 | env: 51 | DOCKER_BUILDKIT: 1 52 | run: | 53 | echo "Building image: ${{ env.FULL_REPO_NAME }}:${{ env.TAG_SHA_SHORT }}" 54 | docker build \ 55 | -t "${{ env.FULL_REPO_NAME }}:${{ env.TAG_SHA_SHORT }}" \ 56 | --ssh default . 57 | 58 | for tag in ${{ env.EXTRA_TAGS }}; do 59 | echo "Adding extra tag: ${{ env.FULL_REPO_NAME }}:$tag" 60 | docker tag "${{ env.FULL_REPO_NAME }}:${{ env.TAG_SHA_SHORT }}" "${{ env.FULL_REPO_NAME }}:$tag" 61 | done 62 | 63 | - name: Push To repository 64 | run: | 65 | echo "Pushing tag: ${{ env.FULL_REPO_NAME }}:${{ env.TAG_SHA_SHORT }}" 66 | docker push "${{ env.FULL_REPO_NAME }}:${{ env.TAG_SHA_SHORT }}" 67 | 68 | for tag in ${{ env.EXTRA_TAGS }}; do 69 | echo "Pushing extra tag: ${{ env.FULL_REPO_NAME }}:$tag" 70 | docker push "${{ env.FULL_REPO_NAME }}:$tag" 71 | done 72 | 73 | - name: Remove Build Images From Runner 74 | if: always() 75 | run: | 76 | echo "Cleaning tag: ${{ env.FULL_REPO_NAME }}:${{ env.TAG_SHA_SHORT }}" 77 | docker image rm "${{ env.FULL_REPO_NAME }}:${{ env.TAG_SHA_SHORT }}" 78 | 79 | for tag in ${{ env.EXTRA_TAGS }}; do 80 | echo "Cleaning extra tag: ${{ env.FULL_REPO_NAME }}:$tag" 81 | docker image rm "${{ env.FULL_REPO_NAME }}:$tag" 82 | done 83 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" # All branches 7 | 8 | permissions: 9 | id-token: write 10 | contents: read 11 | 12 | env: 13 | CGO_ENABLED: 0 14 | 15 | jobs: 16 | lint: 17 | runs-on: [ubuntu-latest] 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: actions/setup-go@v4 22 | with: 23 | go-version: '1.21' 24 | cache: false 25 | 26 | - name: golangci-lint 27 | uses: golangci/golangci-lint-action@v3 28 | with: 29 | version: latest 30 | 31 | dockerfile-lint: 32 | runs-on: [ubuntu-latest] 33 | container: 34 | image: hadolint/hadolint:latest-alpine 35 | steps: 36 | - uses: actions/checkout@v3 37 | 38 | - name: Lint Dockerfile 39 | run: hadolint --ignore DL3008 --ignore DL3018 Dockerfile 40 | 41 | test: 42 | runs-on: [ubuntu-latest] 43 | container: 44 | image: public.ecr.aws/docker/library/golang:1-alpine 45 | steps: 46 | - uses: actions/checkout@v3 47 | 48 | - name: Install tools 49 | run: apk --no-cache --update add openssh-client git make 50 | 51 | - name: Run tests 52 | run: make test 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | bindata.go 3 | server.crt 4 | server.key 5 | local.sh 6 | build 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build UI 2 | FROM node:alpine3.18 as build-ui 3 | WORKDIR /build 4 | COPY ui/package.json . 5 | COPY ui/yarn.lock . 6 | RUN yarn 7 | COPY ui/. . 8 | RUN yarn build 9 | 10 | # Build API 11 | FROM golang:alpine3.18 AS build-api 12 | 13 | RUN apk add --no-cache --update openssh-client git make 14 | ENV CGO_ENABLED=0 15 | ENV GOOS=linux 16 | ENV GOARCH=amd64 17 | 18 | SHELL ["/bin/ash", "-c"] 19 | 20 | # Setup SSH for private repos 21 | RUN mkdir -m 0700 ~/.ssh \ 22 | && ssh-keyscan github.com >> ~/.ssh/known_hosts \ 23 | && git config --global url."git@github.com:".insteadOf "https://github.com/" 24 | 25 | WORKDIR /build 26 | COPY . . 27 | 28 | # Embed the UI 29 | COPY --from=build-ui /build/dist/. embed/public_html 30 | 31 | RUN --mount=type=ssh make 32 | 33 | FROM alpine:3.18 34 | 35 | RUN apk add --no-cache --update tzdata ca-certificates su-exec 36 | 37 | # Copy the executable 38 | WORKDIR /app 39 | COPY --from=build-api /build/build/gorestapi /app/gorestapi 40 | ENTRYPOINT [ "su-exec", "nobody:nobody", "/app/gorestapi" ] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Zach Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXECUTABLE := gorestapi 2 | GITVERSION := $(shell git describe --dirty --always --tags --long) 3 | GOPATH ?= ${HOME}/go 4 | PACKAGENAME := $(shell go list -m -f '{{.Path}}') 5 | TOOLS := ${GOPATH}/bin/mockery \ 6 | ${GOPATH}/bin/swag 7 | SWAGGERSOURCE = $(wildcard gorestapi/*.go) \ 8 | $(wildcard gorestapi/mainrpc/*.go) 9 | 10 | .PHONY: default 11 | default: ${EXECUTABLE} 12 | 13 | tools: ${TOOLS} 14 | 15 | ${GOPATH}/bin/mockery: 16 | go install github.com/vektra/mockery/v2@latest 17 | 18 | ${GOPATH}/bin/swag: 19 | go install github.com/swaggo/swag/cmd/swag@latest 20 | 21 | .PHONY: swagger 22 | swagger: tools ${SWAGGERSOURCE} 23 | swag init --dir . --parseDependency --parseDepth 1 --generalInfo gorestapi/swagger.go --exclude embed --output embed/public_html/api-docs 24 | rm embed/public_html/api-docs/docs.go 25 | rm embed/public_html/api-docs/swagger.yaml 26 | 27 | .PHONY: ${EXECUTABLE} 28 | ${EXECUTABLE}: tools swagger 29 | # Compiling... 30 | mkdir -p build 31 | go build -ldflags "-X github.com/snowzach/golib/version.Executable=${EXECUTABLE} -X github.com/snowzach/golib/version.GitVersion=${GITVERSION}" -o build/${EXECUTABLE} 32 | 33 | .PHONY: mocks 34 | mocks: tools 35 | mockery --dir ./gorestapi --name GRStore 36 | 37 | .PHONY: test 38 | test: tools mocks 39 | go test -cover ./... 40 | 41 | .PHONY: lint 42 | lint: 43 | docker run --rm -v ${PWD}:/app -w /app golangci/golangci-lint:latest golangci-lint run -v --timeout 5m 44 | 45 | .PHONY: hadolint 46 | hadolint: 47 | docker run -it --rm -v ${PWD}/Dockerfile:/Dockerfile hadolint/hadolint:latest hadolint --ignore DL3018 Dockerfile 48 | 49 | .PHONY: relocate 50 | relocate: 51 | @test ${TARGET} || ( echo ">> TARGET is not set. Use: make relocate TARGET="; exit 1 ) 52 | $(eval ESCAPED_PACKAGENAME := $(shell echo "${PACKAGENAME}" | sed -e 's/[\/&]/\\&/g')) 53 | $(eval ESCAPED_TARGET := $(shell echo "${TARGET}" | sed -e 's/[\/&]/\\&/g')) 54 | # Renaming package ${PACKAGENAME} to ${TARGET} 55 | @grep -rlI '${PACKAGENAME}' * | xargs -i@ sed -i 's/${ESCAPED_PACKAGENAME}/${ESCAPED_TARGET}/g' @ 56 | # Complete... 57 | # NOTE: This does not update the git config nor will it update any imports of the root directory of this project. 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Base API Example 2 | 3 | This API example is a basic framework for a REST API 4 | 5 | ## Compiling 6 | This is designed as a go module aware program and thus requires go 1.11 or better 7 | You can clone it anywhere, just run `make` inside the cloned directory to build 8 | 9 | ## Requirements 10 | This does require a postgres database to be setup and reachable. It will attempt to create and migrate the database upon starting. 11 | 12 | ## Configuration 13 | The configuration is designed to be specified with environment variables in all caps with underscores instead of periods. 14 | ``` 15 | Example: 16 | LOGGER_LEVEL=debug 17 | ``` 18 | 19 | ### Options: 20 | | Setting | Description | Default | 21 | | ------------------------------- | ----------------------------------------------------------- | ----------------------- | 22 | | logger.level | The default logging level | "info" | 23 | | logger.encoding | Logging format (console, json or stackdriver) | "console" | 24 | | logger.color | Enable color in console mode | true | 25 | | logger.dev_mode | Dump additional information as part of log messages | true | 26 | | logger.disable_caller | Hide the caller source file and line number | false | 27 | | logger.disable_stacktrace | Hide a stacktrace on debug logs | true | 28 | | --- | --- | --- | 29 | | metrics.enabled | Enable metrics server | true | 30 | | metrics.host | Host/IP to listen on for metrics server | "" | 31 | | metrics.port | Port to listen on for metrics server | 6060 | 32 | | profiler.enabled | Enable go profiler on metrics server under /debug/pprof/ | true | 33 | | pidfile | If set, creates a pidfile at the given path | "" | 34 | | --- | --- | --- | 35 | | server.host | The host address to listen on (blank=all addresses) | "" | 36 | | server.port | The port number to listen on | 8900 | 37 | | server.tls | Enable https/tls | false | 38 | | server.devcert | Generate a development cert | false | 39 | | server.certfile | The HTTPS/TLS server certificate | "server.crt" | 40 | | server.keyfile | The HTTPS/TLS server key file | "server.key" | 41 | | server.log.enabled | Log server requests | true | 42 | | server.log.level | Log level for server requests | "info | 43 | | server.log.request_body | Log the request body | false | 44 | | server.log.response_body | Log the response body | false | 45 | | server.log.ignore_paths | The endpoint prefixes to not log | []string{"/version"} | 46 | | server.cors.enabled | Enable CORS middleware | false | 47 | | server.cors.allowed_origins | CORS Allowed origins | []string{"*"} | 48 | | server.cors.allowed_methods | CORS Allowed methods | []string{...everything} | 49 | | server.cors.allowed_headers | CORS Allowed headers | []string{"*"} | 50 | | server.cors.allowed_credentials | CORS Allowed credentials | false | 51 | | server.cors.max_age | CORS Max Age | 300 | 52 | | server.metrics.enabled | Enable metrics on server endpoints | true | 53 | | server.metrics.ignore_paths | The endpoint prefixes to not capture metrics on | []string{"/version"} | 54 | | --- | --- | --- | 55 | | database.username | The database username | "postgres" | 56 | | database.password | The database password | "password" | 57 | | database.host | Thos hostname for the database | "postgres" | 58 | | database.port | The port for the database | 5432 | 59 | | database.database | The database | "gorestapi" | 60 | | database.auto_create | Automatically create database | true | 61 | | database.search_path | Set the search path | "" | 62 | | database.sslmode | The postgres sslmode to use | "disable" | 63 | | database.sslcert | The postgres sslcert file | "" | 64 | | database.sslkey | The postgres sslkey file | "" | 65 | | database.sslrootcert | The postgres sslrootcert file | "" | 66 | | database.retries | How many times to try to reconnect to the database on start | 7 | 67 | | database.sleep_between_retries | How long to sleep between retries | "7s" | 68 | | database.max_connections | How many pooled connections to have | 40 | 69 | | database.loq_queries | Log queries (must set logging.level=debug) | false | 70 | | database.wipe_confirm | Wipe the database during start | false | 71 | 72 | 73 | ## Data Storage 74 | Data is stored in a postgres database by default. 75 | 76 | ## Query Logic 77 | Find requests `GET /api/things` and `GET /api/widgets` uses a url query parser to allow very complex logic including AND, OR and precedence operators. 78 | For the documentation on how to use this format see https://github.com/snowzach/queryp 79 | 80 | ## Swagger Documentation 81 | When you run the API it has built in Swagger documentation available at `/api/api-docs/` (trailing slash required) 82 | The documentation is automatically generated. 83 | 84 | ## TLS/HTTPS 85 | You can enable https by setting the config option server.tls = true and pointing it to your keyfile and certfile. 86 | To create a self-signed cert: `openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -keyout server.key -out server.crt` 87 | It also has the option to automatically generate a development cert every time it runs using the server.devcert option. 88 | 89 | ## Relocation 90 | If you want to start with this as boilerplate for your project, you can clone this repo and use the `make relocate` option to rename the package. 91 | `make relocate TARGET=github.com/myname/mycoolproject` 92 | -------------------------------------------------------------------------------- /cmd/api.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/go-chi/chi/v5" 10 | "github.com/go-chi/chi/v5/middleware" 11 | "github.com/go-chi/cors" 12 | cli "github.com/spf13/cobra" 13 | 14 | "github.com/snowzach/golib/conf" 15 | "github.com/snowzach/golib/httpserver" 16 | "github.com/snowzach/golib/httpserver/logger" 17 | "github.com/snowzach/golib/httpserver/metrics" 18 | "github.com/snowzach/golib/log" 19 | "github.com/snowzach/golib/signal" 20 | "github.com/snowzach/golib/version" 21 | "github.com/snowzach/gorestapi/embed" 22 | "github.com/snowzach/gorestapi/gorestapi/mainrpc" 23 | "github.com/snowzach/gorestapi/store/postgres" 24 | ) 25 | 26 | func init() { 27 | rootCmd.AddCommand(apiCmd) 28 | } 29 | 30 | var ( 31 | apiCmd = &cli.Command{ 32 | Use: "api", 33 | Short: "Start API", 34 | Long: `Start API`, 35 | Run: func(cmd *cli.Command, args []string) { // Initialize the databse 36 | 37 | var err error 38 | 39 | // Create the router and server config 40 | router, err := newRouter() 41 | if err != nil { 42 | log.Fatalf("router config error: %v", err) 43 | } 44 | 45 | // Create the database 46 | db, err := newDatabase() 47 | if err != nil { 48 | log.Fatalf("database config error: %v", err) 49 | } 50 | 51 | // Version endpoint 52 | router.Get("/version", version.GetVersion()) 53 | 54 | // MainRPC 55 | if err = mainrpc.Setup(router, db); err != nil { 56 | log.Fatalf("Could not setup mainrpc: %v", err) 57 | } 58 | 59 | // Serve embedded public html 60 | htmlFilesFS := embed.PublicHTMLFS() 61 | htmlFilesServer := http.FileServer(http.FS(htmlFilesFS)) 62 | // Serve swagger docs 63 | router.Mount("/api-docs", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 64 | w.Header().Set("Vary", "Accept-Encoding") 65 | w.Header().Set("Cache-Control", "no-cache") 66 | htmlFilesServer.ServeHTTP(w, r) 67 | })) 68 | // Serve embedded webapp 69 | router.Mount("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 | // See if the file exists 71 | file, err := htmlFilesFS.Open(strings.TrimLeft(r.URL.Path, "/")) 72 | if err != nil { 73 | // If the file is not found, serve the root index.html file 74 | r.URL.Path = "/" 75 | } else { 76 | file.Close() 77 | } 78 | w.Header().Set("Vary", "Accept-Encoding") 79 | w.Header().Set("Cache-Control", "no-cache") 80 | htmlFilesServer.ServeHTTP(w, r) 81 | })) 82 | 83 | // Create a server 84 | s, err := newServer(router) 85 | if err != nil { 86 | log.Fatalf("could not create server error: %v", err) 87 | } 88 | 89 | // Start the listener and service connections. 90 | go func() { 91 | if err = s.ListenAndServe(); err != nil { 92 | log.Errorf("Server error: %v", err) 93 | signal.Stop.Stop() 94 | } 95 | }() 96 | log.Infof("API listening on %s", s.Addr) 97 | 98 | // Register signal handler and wait 99 | signal.Stop.OnSignal(signal.DefaultStopSignals...) 100 | <-signal.Stop.Chan() // Wait until Stop 101 | signal.Stop.Wait() // Wait until everyone cleans up 102 | }, 103 | } 104 | ) 105 | 106 | func newRouter() (chi.Router, error) { 107 | 108 | router := chi.NewRouter() 109 | router.Use( 110 | middleware.Recoverer, // Recover from panics 111 | middleware.RequestID, // Inject request-id 112 | ) 113 | 114 | // Request logger 115 | if conf.C.Bool("server.log.enabled") { 116 | var loggerConfig logger.Config 117 | if err := conf.C.Unmarshal(&loggerConfig, conf.UnmarshalConf{Path: "server.log"}); err != nil { 118 | return nil, fmt.Errorf("could not parser server.log config: %w", err) 119 | } 120 | switch conf.C.String("logger.encoding") { 121 | default: 122 | router.Use(logger.LoggerStandardMiddleware(log.Logger.With("context", "server"), loggerConfig)) 123 | } 124 | } 125 | 126 | // CORS handler 127 | if conf.C.Bool("server.cors.enabled") { 128 | var corsOptions cors.Options 129 | if err := conf.C.Unmarshal(&corsOptions, conf.UnmarshalConf{ 130 | Path: "server.cors", 131 | DecoderConfig: conf.DefaultDecoderConfig( 132 | conf.WithMatchName(conf.MatchSnakeCaseConfig), 133 | ), 134 | }); err != nil { 135 | return nil, fmt.Errorf("could not parser server.cors config: %w", err) 136 | } 137 | router.Use(cors.New(corsOptions).Handler) 138 | } 139 | 140 | // If we have server metrics enabled, enable the middleware to collect them on the server. 141 | if conf.C.Bool("server.metrics.enabled") { 142 | var metricsConfig metrics.Config 143 | if err := conf.C.Unmarshal(&metricsConfig, conf.UnmarshalConf{ 144 | Path: "server.metrics", 145 | DecoderConfig: conf.DefaultDecoderConfig(), 146 | }); err != nil { 147 | return nil, fmt.Errorf("could not parser server.metrics config: %w", err) 148 | } 149 | router.Use(metrics.MetricsMiddleware(metricsConfig)) 150 | } 151 | 152 | return router, nil 153 | 154 | } 155 | 156 | func newServer(handler http.Handler) (*httpserver.Server, error) { 157 | 158 | // Parse the config 159 | var serverConfig = &httpserver.Config{Handler: handler} 160 | if err := conf.C.Unmarshal(serverConfig, conf.UnmarshalConf{Path: "server"}); err != nil { 161 | return nil, fmt.Errorf("could not parse server config: %w", err) 162 | } 163 | 164 | // Create the server 165 | s, err := httpserver.New(httpserver.WithConfig(serverConfig)) 166 | if err != nil { 167 | return nil, fmt.Errorf("could not create server: %w", err) 168 | } 169 | 170 | return s, nil 171 | 172 | } 173 | 174 | func newDatabase() (*postgres.Client, error) { 175 | 176 | var err error 177 | 178 | // Database config 179 | var postgresConfig = &postgres.Config{} 180 | if err := conf.C.Unmarshal(postgresConfig, conf.UnmarshalConf{Path: "database"}); err != nil { 181 | return nil, fmt.Errorf("could not parse database config: %v", err) 182 | } 183 | 184 | // Loggers 185 | postgresConfig.Logger = log.NewWrapper(log.Logger.With("context", "database.postgres"), slog.LevelInfo) 186 | if conf.C.Bool("database.log_queries") { 187 | postgresConfig.QueryLogger = log.NewWrapper(log.Logger.With("context", "database.postgres.query"), slog.LevelDebug) 188 | } 189 | 190 | // Migrations 191 | postgresConfig.MigrationSource, err = embed.MigrationSource() 192 | if err != nil { 193 | return nil, fmt.Errorf("could not get database migrations error: %w", err) 194 | } 195 | 196 | // Create database 197 | db, err := postgres.New(postgresConfig) 198 | if err != nil { 199 | return nil, fmt.Errorf("could not create database client: %w", err) 200 | } 201 | 202 | return db, nil 203 | 204 | } 205 | -------------------------------------------------------------------------------- /cmd/defaults.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "net/http" 4 | 5 | // defaults loads the default config for the app 6 | func defaults() map[string]interface{} { 7 | return map[string]interface{}{ 8 | // Logger Defaults 9 | "logger.level": "info", 10 | "logger.encoding": "console", 11 | "logger.color": true, 12 | "logger.output": "stderr", 13 | 14 | // Metrics, profiler, pidfile 15 | "metrics.enabled": true, 16 | "metrics.host": "", 17 | "metrics.port": "6060", 18 | "profiler.enabled": true, 19 | "pidfile": "", 20 | 21 | // Server Configuration 22 | "server.host": "", 23 | "server.port": "8080", 24 | "server.tls": false, 25 | "server.devcert": false, 26 | "server.certfile": "server.crt", 27 | "server.keyfile": "server.key", 28 | // Server Log 29 | "server.log.enabled": true, 30 | "server.log.level": "info", 31 | "server.log.request_body": false, 32 | "server.log.response_body": false, 33 | "server.log.ignore_paths": []string{"/version"}, 34 | // Server CORS 35 | "server.cors.enabled": true, 36 | "server.cors.allowed_origins": []string{"*"}, 37 | "server.cors.allowed_methods": []string{http.MethodHead, http.MethodOptions, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch}, 38 | "server.cors.allowed_headers": []string{"*"}, 39 | "server.cors.allow_credentials": false, 40 | "server.cors.max_age": 300, 41 | // Server Metrics 42 | "server.metrics.enabled": true, 43 | "server.metrics.ignore_paths": []string{"/version"}, 44 | 45 | // Database Settings 46 | "database.username": "postgres", 47 | "database.password": "postgres", 48 | "database.host": "postgres", 49 | "database.port": 5432, 50 | "database.database": "gorestapi", 51 | "database.auto_create": true, 52 | "database.schema": "public", 53 | "database.search_path": "", 54 | "database.sslmode": "disable", 55 | "database.sslcert": "", 56 | "database.sslkey": "", 57 | "database.sslrootcert": "", 58 | "database.retries": 5, 59 | "database.sleep_between_retries": "7s", 60 | "database.max_connections": 40, 61 | "database.log_queries": false, 62 | "database.wipe_confirm": false, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/http/pprof" 8 | "os" 9 | 10 | "github.com/prometheus/client_golang/prometheus/promhttp" 11 | cli "github.com/spf13/cobra" 12 | 13 | "github.com/snowzach/golib/conf" 14 | "github.com/snowzach/golib/log" 15 | "github.com/snowzach/golib/version" 16 | ) 17 | 18 | func init() { 19 | rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file") 20 | } 21 | 22 | var ( 23 | 24 | // Config and global logger 25 | pidFile string 26 | cfgFile string 27 | 28 | // The Root Cli Handler 29 | rootCmd = &cli.Command{ 30 | Version: version.GitVersion, 31 | Use: version.Executable, 32 | PersistentPreRunE: func(cmd *cli.Command, args []string) error { 33 | 34 | // Parse defaults, config file and environment. 35 | if err := conf.C.Parse( 36 | conf.WithMap(defaults()), 37 | conf.WithFile(cfgFile), 38 | conf.WithEnv(), 39 | ); err != nil { 40 | fmt.Printf("could not load config: %v", err) 41 | os.Exit(1) 42 | } 43 | 44 | var loggerConfig log.LoggerConfig 45 | if err := conf.C.Unmarshal(&loggerConfig, conf.UnmarshalConf{Path: "logger"}); err != nil { 46 | fmt.Printf("could not parse logger config: %v", err) 47 | os.Exit(1) 48 | } 49 | if err := log.InitLogger(&loggerConfig); err != nil { 50 | fmt.Printf("could not configure logger: %v", err) 51 | os.Exit(1) 52 | } 53 | 54 | // Load the metrics server 55 | if conf.C.Bool("metrics.enabled") { 56 | hostPort := net.JoinHostPort(conf.C.String("metrics.host"), conf.C.String("metrics.port")) 57 | r := http.NewServeMux() 58 | r.Handle("/metrics", promhttp.Handler()) 59 | if conf.C.Bool("profiler.enabled") { 60 | r.HandleFunc("/debug/pprof/", pprof.Index) 61 | r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 62 | r.HandleFunc("/debug/pprof/profile", pprof.Profile) 63 | r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 64 | r.HandleFunc("/debug/pprof/trace", pprof.Trace) 65 | log.Info("Profiler enabled", "profiler_path", fmt.Sprintf("http://%s/debug/pprof/", hostPort)) 66 | } 67 | go func() { 68 | if err := http.ListenAndServe(hostPort, r); err != nil { 69 | log.Errorf("Metrics server error: %v", err) 70 | } 71 | }() 72 | log.Info("Metrics enabled", "address", hostPort) 73 | } 74 | 75 | // Create Pid File 76 | pidFile = conf.C.String("pidfile") 77 | if pidFile != "" { 78 | file, err := os.OpenFile(pidFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) 79 | if err != nil { 80 | return fmt.Errorf("could not create pid file: %s error:%v", pidFile, err) 81 | } 82 | defer file.Close() 83 | _, err = fmt.Fprintf(file, "%d\n", os.Getpid()) 84 | if err != nil { 85 | return fmt.Errorf("could not create pid file: %s error:%v", pidFile, err) 86 | } 87 | } 88 | return nil 89 | }, 90 | PersistentPostRun: func(cmd *cli.Command, args []string) { 91 | // Remove Pid file 92 | if pidFile != "" { 93 | os.Remove(pidFile) 94 | } 95 | }, 96 | } 97 | ) 98 | 99 | // Execute starts the program 100 | func Execute() { 101 | if err := rootCmd.Execute(); err != nil { 102 | fmt.Fprintf(os.Stderr, "%v\n", err) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | cli "github.com/spf13/cobra" 7 | 8 | "github.com/snowzach/golib/version" 9 | ) 10 | 11 | // Version command 12 | func init() { 13 | rootCmd.AddCommand(&cli.Command{ 14 | Use: "version", 15 | Short: "Show version", 16 | Long: `Show version`, 17 | Run: func(cmd *cli.Command, args []string) { 18 | fmt.Println(version.Executable + " - " + version.GitVersion) 19 | }, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /embed/embed.go: -------------------------------------------------------------------------------- 1 | package embed 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | 7 | "github.com/golang-migrate/migrate/v4/source" 8 | "github.com/golang-migrate/migrate/v4/source/iofs" 9 | ) 10 | 11 | //go:embed postgres_migrations 12 | var postgresMigrations embed.FS 13 | 14 | func MigrationSource() (source.Driver, error) { 15 | return iofs.New(postgresMigrations, "postgres_migrations") 16 | } 17 | 18 | //go:embed public_html 19 | var publicHTML embed.FS 20 | 21 | func PublicHTMLFS() fs.FS { 22 | publicHTMLfs, _ := fs.Sub(publicHTML, "public_html") 23 | return publicHTMLfs 24 | } 25 | -------------------------------------------------------------------------------- /embed/postgres_migrations/0001_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE widget; 2 | DROP TABLE thing; 3 | -------------------------------------------------------------------------------- /embed/postgres_migrations/0001_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS thing ( 2 | id TEXT PRIMARY KEY NOT NULL, 3 | created timestamp with time zone default NOW(), 4 | updated timestamp with time zone default NOW(), 5 | name TEXT, 6 | description TEXT 7 | ); 8 | 9 | CREATE TABLE IF NOT EXISTS widget ( 10 | id TEXT PRIMARY KEY NOT NULL, 11 | created timestamp with time zone default NOW(), 12 | updated timestamp with time zone default NOW(), 13 | name TEXT, 14 | description TEXT, 15 | thing_id TEXT 16 | ); 17 | 18 | ALTER TABLE ONLY widget ADD CONSTRAINT fkey_widget_thing_id FOREIGN KEY (thing_id) REFERENCES public.thing(id) ON DELETE CASCADE; 19 | -------------------------------------------------------------------------------- /embed/public_html/api-docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | API Docs 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Please wait for API documents to load... 14 | 15 | -------------------------------------------------------------------------------- /embed/public_html/api-docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample server celler server.", 5 | "title": "Swagger Example API", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "name": "API Support", 9 | "url": "http://www.swagger.io/support", 10 | "email": "support@swagger.io" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | }, 16 | "version": "1.0" 17 | }, 18 | "host": "localhost:8080", 19 | "basePath": "/api/v1", 20 | "paths": { 21 | "/things": { 22 | "get": { 23 | "description": "Find things", 24 | "consumes": [ 25 | "application/json" 26 | ], 27 | "produces": [ 28 | "application/json" 29 | ], 30 | "tags": [ 31 | "Things" 32 | ], 33 | "summary": "Find things", 34 | "operationId": "ThingsFind", 35 | "parameters": [ 36 | { 37 | "type": "string", 38 | "description": "id", 39 | "name": "id", 40 | "in": "query" 41 | }, 42 | { 43 | "type": "string", 44 | "description": "name", 45 | "name": "name", 46 | "in": "query" 47 | }, 48 | { 49 | "type": "string", 50 | "description": "description", 51 | "name": "description", 52 | "in": "query" 53 | }, 54 | { 55 | "type": "integer", 56 | "description": "offset", 57 | "name": "offset", 58 | "in": "query" 59 | }, 60 | { 61 | "type": "integer", 62 | "description": "limit", 63 | "name": "limit", 64 | "in": "query" 65 | }, 66 | { 67 | "type": "string", 68 | "description": "query", 69 | "name": "sort", 70 | "in": "query" 71 | } 72 | ], 73 | "responses": { 74 | "200": { 75 | "description": "OK", 76 | "schema": { 77 | "type": "array", 78 | "items": { 79 | "$ref": "#/definitions/gorestapi.Thing" 80 | } 81 | } 82 | }, 83 | "400": { 84 | "description": "Invalid Argument", 85 | "schema": { 86 | "$ref": "#/definitions/render.ErrResponse" 87 | } 88 | }, 89 | "500": { 90 | "description": "Internal Error", 91 | "schema": { 92 | "$ref": "#/definitions/render.ErrResponse" 93 | } 94 | } 95 | } 96 | }, 97 | "post": { 98 | "description": "Save a thing", 99 | "consumes": [ 100 | "application/json" 101 | ], 102 | "produces": [ 103 | "application/json" 104 | ], 105 | "tags": [ 106 | "Things" 107 | ], 108 | "summary": "Save thing", 109 | "operationId": "ThingSave", 110 | "parameters": [ 111 | { 112 | "description": "Thing", 113 | "name": "thing", 114 | "in": "body", 115 | "required": true, 116 | "schema": { 117 | "$ref": "#/definitions/gorestapi.ThingExample" 118 | } 119 | } 120 | ], 121 | "responses": { 122 | "200": { 123 | "description": "OK", 124 | "schema": { 125 | "$ref": "#/definitions/gorestapi.Thing" 126 | } 127 | }, 128 | "400": { 129 | "description": "Invalid Argument", 130 | "schema": { 131 | "$ref": "#/definitions/render.ErrResponse" 132 | } 133 | }, 134 | "500": { 135 | "description": "Internal Error", 136 | "schema": { 137 | "$ref": "#/definitions/render.ErrResponse" 138 | } 139 | } 140 | } 141 | } 142 | }, 143 | "/things/{id}": { 144 | "get": { 145 | "description": "Get a thing", 146 | "consumes": [ 147 | "application/json" 148 | ], 149 | "produces": [ 150 | "application/json" 151 | ], 152 | "tags": [ 153 | "Things" 154 | ], 155 | "summary": "Get thing", 156 | "operationId": "ThingGetByID", 157 | "parameters": [ 158 | { 159 | "type": "string", 160 | "description": "ID", 161 | "name": "id", 162 | "in": "path", 163 | "required": true 164 | } 165 | ], 166 | "responses": { 167 | "200": { 168 | "description": "OK", 169 | "schema": { 170 | "$ref": "#/definitions/gorestapi.Thing" 171 | } 172 | }, 173 | "400": { 174 | "description": "Invalid Argument", 175 | "schema": { 176 | "$ref": "#/definitions/render.ErrResponse" 177 | } 178 | }, 179 | "404": { 180 | "description": "Not Found", 181 | "schema": { 182 | "$ref": "#/definitions/render.ErrResponse" 183 | } 184 | }, 185 | "500": { 186 | "description": "Internal Error", 187 | "schema": { 188 | "$ref": "#/definitions/render.ErrResponse" 189 | } 190 | } 191 | } 192 | }, 193 | "delete": { 194 | "description": "Delete a thing", 195 | "consumes": [ 196 | "application/json" 197 | ], 198 | "produces": [ 199 | "application/json" 200 | ], 201 | "tags": [ 202 | "Things" 203 | ], 204 | "summary": "Delete thing", 205 | "operationId": "ThingDeleteByID", 206 | "parameters": [ 207 | { 208 | "type": "string", 209 | "description": "ID", 210 | "name": "id", 211 | "in": "path", 212 | "required": true 213 | } 214 | ], 215 | "responses": { 216 | "204": { 217 | "description": "Success" 218 | }, 219 | "400": { 220 | "description": "Invalid Argument", 221 | "schema": { 222 | "$ref": "#/definitions/render.ErrResponse" 223 | } 224 | }, 225 | "404": { 226 | "description": "Not Found", 227 | "schema": { 228 | "$ref": "#/definitions/render.ErrResponse" 229 | } 230 | }, 231 | "500": { 232 | "description": "Internal Error", 233 | "schema": { 234 | "$ref": "#/definitions/render.ErrResponse" 235 | } 236 | } 237 | } 238 | } 239 | }, 240 | "/widgets": { 241 | "get": { 242 | "description": "Find widgets", 243 | "consumes": [ 244 | "application/json" 245 | ], 246 | "produces": [ 247 | "application/json" 248 | ], 249 | "tags": [ 250 | "Widgets" 251 | ], 252 | "summary": "Find widgets", 253 | "operationId": "WidgetsFind", 254 | "parameters": [ 255 | { 256 | "type": "string", 257 | "description": "id", 258 | "name": "id", 259 | "in": "query" 260 | }, 261 | { 262 | "type": "string", 263 | "description": "name", 264 | "name": "name", 265 | "in": "query" 266 | }, 267 | { 268 | "type": "string", 269 | "description": "description", 270 | "name": "description", 271 | "in": "query" 272 | }, 273 | { 274 | "type": "integer", 275 | "description": "offset", 276 | "name": "offset", 277 | "in": "query" 278 | }, 279 | { 280 | "type": "integer", 281 | "description": "limit", 282 | "name": "limit", 283 | "in": "query" 284 | }, 285 | { 286 | "type": "string", 287 | "description": "query", 288 | "name": "sort", 289 | "in": "query" 290 | } 291 | ], 292 | "responses": { 293 | "200": { 294 | "description": "OK", 295 | "schema": { 296 | "type": "array", 297 | "items": { 298 | "$ref": "#/definitions/gorestapi.Widget" 299 | } 300 | } 301 | }, 302 | "400": { 303 | "description": "Invalid Argument", 304 | "schema": { 305 | "$ref": "#/definitions/render.ErrResponse" 306 | } 307 | }, 308 | "500": { 309 | "description": "Internal Error", 310 | "schema": { 311 | "$ref": "#/definitions/render.ErrResponse" 312 | } 313 | } 314 | } 315 | }, 316 | "post": { 317 | "description": "Save a widget", 318 | "consumes": [ 319 | "application/json" 320 | ], 321 | "produces": [ 322 | "application/json" 323 | ], 324 | "tags": [ 325 | "Widgets" 326 | ], 327 | "summary": "Save widget", 328 | "operationId": "WidgetSave", 329 | "parameters": [ 330 | { 331 | "description": "Widget", 332 | "name": "widget", 333 | "in": "body", 334 | "required": true, 335 | "schema": { 336 | "$ref": "#/definitions/gorestapi.WidgetExample" 337 | } 338 | } 339 | ], 340 | "responses": { 341 | "200": { 342 | "description": "OK", 343 | "schema": { 344 | "$ref": "#/definitions/gorestapi.Widget" 345 | } 346 | }, 347 | "400": { 348 | "description": "Invalid Argument", 349 | "schema": { 350 | "$ref": "#/definitions/render.ErrResponse" 351 | } 352 | }, 353 | "500": { 354 | "description": "Internal Error", 355 | "schema": { 356 | "$ref": "#/definitions/render.ErrResponse" 357 | } 358 | } 359 | } 360 | } 361 | }, 362 | "/widgets/{id}": { 363 | "get": { 364 | "description": "Get a widget", 365 | "consumes": [ 366 | "application/json" 367 | ], 368 | "produces": [ 369 | "application/json" 370 | ], 371 | "tags": [ 372 | "Widgets" 373 | ], 374 | "summary": "Get widget", 375 | "operationId": "WidgetGetByID", 376 | "parameters": [ 377 | { 378 | "type": "string", 379 | "description": "ID", 380 | "name": "id", 381 | "in": "path", 382 | "required": true 383 | } 384 | ], 385 | "responses": { 386 | "200": { 387 | "description": "OK", 388 | "schema": { 389 | "$ref": "#/definitions/gorestapi.Widget" 390 | } 391 | }, 392 | "400": { 393 | "description": "Invalid Argument", 394 | "schema": { 395 | "$ref": "#/definitions/render.ErrResponse" 396 | } 397 | }, 398 | "404": { 399 | "description": "Not Found", 400 | "schema": { 401 | "$ref": "#/definitions/render.ErrResponse" 402 | } 403 | }, 404 | "500": { 405 | "description": "Internal Error", 406 | "schema": { 407 | "$ref": "#/definitions/render.ErrResponse" 408 | } 409 | } 410 | } 411 | }, 412 | "delete": { 413 | "description": "Delete a widget", 414 | "consumes": [ 415 | "application/json" 416 | ], 417 | "produces": [ 418 | "application/json" 419 | ], 420 | "tags": [ 421 | "Widgets" 422 | ], 423 | "summary": "Delete widget", 424 | "operationId": "WidgetDeleteByID", 425 | "parameters": [ 426 | { 427 | "type": "string", 428 | "description": "ID", 429 | "name": "id", 430 | "in": "path", 431 | "required": true 432 | } 433 | ], 434 | "responses": { 435 | "204": { 436 | "description": "Success" 437 | }, 438 | "400": { 439 | "description": "Invalid Argument", 440 | "schema": { 441 | "$ref": "#/definitions/render.ErrResponse" 442 | } 443 | }, 444 | "404": { 445 | "description": "Not Found", 446 | "schema": { 447 | "$ref": "#/definitions/render.ErrResponse" 448 | } 449 | }, 450 | "500": { 451 | "description": "Internal Error", 452 | "schema": { 453 | "$ref": "#/definitions/render.ErrResponse" 454 | } 455 | } 456 | } 457 | } 458 | } 459 | }, 460 | "definitions": { 461 | "gorestapi.Thing": { 462 | "type": "object", 463 | "properties": { 464 | "created": { 465 | "description": "Created Timestamp", 466 | "type": "string" 467 | }, 468 | "description": { 469 | "description": "Description", 470 | "type": "string" 471 | }, 472 | "id": { 473 | "description": "ID (Auto-Generated)", 474 | "type": "string" 475 | }, 476 | "name": { 477 | "description": "Name", 478 | "type": "string" 479 | }, 480 | "updated": { 481 | "description": "Updated Timestamp", 482 | "type": "string" 483 | } 484 | } 485 | }, 486 | "gorestapi.ThingExample": { 487 | "type": "object", 488 | "properties": { 489 | "description": { 490 | "description": "Description", 491 | "type": "string" 492 | }, 493 | "name": { 494 | "description": "Name", 495 | "type": "string" 496 | } 497 | } 498 | }, 499 | "gorestapi.Widget": { 500 | "type": "object", 501 | "properties": { 502 | "created": { 503 | "description": "Created Timestamp", 504 | "type": "string" 505 | }, 506 | "description": { 507 | "description": "Description", 508 | "type": "string" 509 | }, 510 | "id": { 511 | "description": "ID (Auto-Generated)", 512 | "type": "string" 513 | }, 514 | "name": { 515 | "description": "Name", 516 | "type": "string" 517 | }, 518 | "thing": { 519 | "description": "Loaded Structs", 520 | "allOf": [ 521 | { 522 | "$ref": "#/definitions/gorestapi.Thing" 523 | } 524 | ] 525 | }, 526 | "thing_id": { 527 | "description": "ThingID", 528 | "type": "string" 529 | }, 530 | "updated": { 531 | "description": "Updated Timestamp", 532 | "type": "string" 533 | } 534 | } 535 | }, 536 | "gorestapi.WidgetExample": { 537 | "type": "object", 538 | "properties": { 539 | "description": { 540 | "description": "Description", 541 | "type": "string" 542 | }, 543 | "name": { 544 | "description": "Name", 545 | "type": "string" 546 | }, 547 | "thing_id": { 548 | "description": "ThingID", 549 | "type": "string" 550 | } 551 | } 552 | }, 553 | "render.ErrResponse": { 554 | "type": "object", 555 | "properties": { 556 | "error": { 557 | "type": "string" 558 | }, 559 | "error_id": { 560 | "type": "string" 561 | }, 562 | "request_id": { 563 | "type": "string" 564 | }, 565 | "status": { 566 | "type": "string" 567 | } 568 | } 569 | } 570 | }, 571 | "securityDefinitions": { 572 | "ApiKeyAuth": { 573 | "type": "apiKey", 574 | "name": "Authorization", 575 | "in": "header" 576 | }, 577 | "BasicAuth": { 578 | "type": "basic" 579 | }, 580 | "OAuth2AccessCode": { 581 | "type": "oauth2", 582 | "flow": "accessCode", 583 | "authorizationUrl": "https://example.com/oauth/authorize", 584 | "tokenUrl": "https://example.com/oauth/token", 585 | "scopes": { 586 | "admin": " Grants read and write access to administrative information" 587 | } 588 | }, 589 | "OAuth2Application": { 590 | "type": "oauth2", 591 | "flow": "application", 592 | "tokenUrl": "https://example.com/oauth/token", 593 | "scopes": { 594 | "admin": " Grants read and write access to administrative information", 595 | "write": " Grants write access" 596 | } 597 | }, 598 | "OAuth2Implicit": { 599 | "type": "oauth2", 600 | "flow": "implicit", 601 | "authorizationUrl": "https://example.com/oauth/authorize", 602 | "scopes": { 603 | "admin": " Grants read and write access to administrative information", 604 | "write": " Grants write access" 605 | } 606 | }, 607 | "OAuth2Password": { 608 | "type": "oauth2", 609 | "flow": "password", 610 | "tokenUrl": "https://example.com/oauth/token", 611 | "scopes": { 612 | "admin": " Grants read and write access to administrative information", 613 | "read": " Grants read access", 614 | "write": " Grants write access" 615 | } 616 | } 617 | }, 618 | "x-extension-openapi": { 619 | "example": "value on a json format" 620 | } 621 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/snowzach/gorestapi 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gavv/httpexpect/v2 v2.1.0 7 | github.com/go-chi/chi/v5 v5.0.10 8 | github.com/go-chi/cors v1.2.1 9 | github.com/golang-migrate/migrate/v4 v4.16.2 10 | github.com/jmoiron/sqlx v1.3.5 11 | github.com/prometheus/client_golang v1.17.0 12 | github.com/rs/xid v1.5.0 13 | github.com/snowzach/golib v1.0.4 14 | github.com/snowzach/queryp v0.3.6 15 | github.com/spf13/cobra v1.7.0 16 | github.com/stretchr/testify v1.8.2 17 | ) 18 | 19 | require ( 20 | github.com/ajg/form v1.5.1 // indirect 21 | github.com/andybalholm/brotli v1.0.4 // indirect 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 24 | github.com/creasty/defaults v1.7.0 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/fatih/structs v1.1.0 // indirect 27 | github.com/fsnotify/fsnotify v1.7.0 // indirect 28 | github.com/google/go-querystring v1.1.0 // indirect 29 | github.com/gorilla/websocket v1.4.2 // indirect 30 | github.com/hashicorp/errwrap v1.1.0 // indirect 31 | github.com/hashicorp/go-multierror v1.1.1 // indirect 32 | github.com/imkira/go-interpol v1.0.0 // indirect 33 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 34 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 35 | github.com/jackc/pgconn v1.14.1 // indirect 36 | github.com/jackc/pgio v1.0.0 // indirect 37 | github.com/jackc/pgpassfile v1.0.0 // indirect 38 | github.com/jackc/pgproto3/v2 v2.3.2 // indirect 39 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 40 | github.com/jackc/pgtype v1.14.0 // indirect 41 | github.com/jackc/pgx/v4 v4.18.1 // indirect 42 | github.com/klauspost/compress v1.15.11 // indirect 43 | github.com/knadh/koanf v1.5.0 // indirect 44 | github.com/lib/pq v1.10.9 // indirect 45 | github.com/lmittmann/tint v1.0.2 // indirect 46 | github.com/mattn/go-colorable v0.1.12 // indirect 47 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 48 | github.com/mitchellh/copystructure v1.2.0 // indirect 49 | github.com/mitchellh/mapstructure v1.5.0 // indirect 50 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 51 | github.com/nxadm/tail v1.4.8 // indirect 52 | github.com/pelletier/go-toml v1.9.5 // indirect 53 | github.com/pmezard/go-difflib v1.0.0 // indirect 54 | github.com/prometheus/client_model v0.5.0 // indirect 55 | github.com/prometheus/common v0.45.0 // indirect 56 | github.com/prometheus/procfs v0.12.0 // indirect 57 | github.com/sergi/go-diff v1.0.0 // indirect 58 | github.com/shopspring/decimal v1.3.1 // indirect 59 | github.com/snowzach/certtools v1.0.2 // indirect 60 | github.com/spf13/cast v1.5.1 // indirect 61 | github.com/spf13/pflag v1.0.5 // indirect 62 | github.com/stretchr/objx v0.5.0 // indirect 63 | github.com/valyala/bytebufferpool v1.0.0 // indirect 64 | github.com/valyala/fasthttp v1.34.0 // indirect 65 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 66 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 67 | github.com/xeipuuv/gojsonschema v1.1.0 // indirect 68 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect 69 | github.com/yudai/gojsondiff v1.0.0 // indirect 70 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect 71 | go.uber.org/atomic v1.11.0 // indirect 72 | golang.org/x/crypto v0.14.0 // indirect 73 | golang.org/x/net v0.17.0 // indirect 74 | golang.org/x/sys v0.13.0 // indirect 75 | golang.org/x/text v0.13.0 // indirect 76 | google.golang.org/protobuf v1.31.0 // indirect 77 | gopkg.in/yaml.v3 v3.0.1 // indirect 78 | moul.io/http2curl v1.0.1-0.20190925090545-5cd742060b0e // indirect 79 | ) 80 | 81 | //replace github.com/snowzach/queryp => ../queryp 82 | //replace github.com/snowzach/golib => ../golib 83 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 4 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 7 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 8 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 9 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 10 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= 11 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 12 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 13 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 14 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 15 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 16 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 17 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 18 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 19 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 20 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 21 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 22 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 23 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 24 | github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= 25 | github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= 26 | github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= 27 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= 28 | github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= 29 | github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= 30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= 31 | github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= 32 | github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= 33 | github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= 34 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 35 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 36 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 37 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 38 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 39 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 40 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 41 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 42 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 43 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 44 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 45 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 46 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 47 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 48 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 49 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 50 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 51 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 52 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 53 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 54 | github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= 55 | github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 56 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 58 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 59 | github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= 60 | github.com/dhui/dktest v0.3.16/go.mod h1:gYaA3LRmM8Z4vJl2MA0THIigJoZrwOansEOsp+kqxp0= 61 | github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= 62 | github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 63 | github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= 64 | github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 65 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 66 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 67 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 68 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 69 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 70 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 71 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 72 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 73 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 74 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 75 | github.com/fasthttp/websocket v1.4.2 h1:AU/zSiIIAuJjBMf5o+vO0syGOnEfvZRu40xIhW/3RuM= 76 | github.com/fasthttp/websocket v1.4.2/go.mod h1:smsv/h4PBEBaU0XDTY5UwJTpZv69fQ0FfcLJr21mA6Y= 77 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 78 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 79 | github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 80 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 81 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 82 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 83 | github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 84 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 85 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 86 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 87 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 88 | github.com/gavv/httpexpect/v2 v2.1.0 h1:Q7xnFuKqBY2si4DsqxdbWBt9rfrbVTT2/9YSomc9tEw= 89 | github.com/gavv/httpexpect/v2 v2.1.0/go.mod h1:lnd0TqJLrP+wkJk3SFwtrpSlOAZQ7HaaIFuOYbgqgUM= 90 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 91 | github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= 92 | github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 93 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 94 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 95 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 96 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 97 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 98 | github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= 99 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 100 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 101 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 102 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 103 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 104 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 105 | github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 106 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 107 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 108 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 109 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 110 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 111 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 112 | github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= 113 | github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= 114 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 115 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 116 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 117 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 118 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 119 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 120 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 121 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 122 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 123 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 124 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 125 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 126 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 127 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 128 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 129 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 130 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 131 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 132 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 133 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 134 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 135 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 136 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 137 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 140 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 141 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 142 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 143 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 144 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 145 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 146 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 147 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 148 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 149 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 150 | github.com/gorilla/websocket v1.0.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 151 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 152 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 153 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 154 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 155 | github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= 156 | github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= 157 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 158 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 159 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 160 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 161 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 162 | github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= 163 | github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 164 | github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 165 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 166 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 167 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 168 | github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 169 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 170 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 171 | github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= 172 | github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 173 | github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 174 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 175 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 176 | github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= 177 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 178 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 179 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 180 | github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 181 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 182 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 183 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 184 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 185 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 186 | github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= 187 | github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= 188 | github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= 189 | github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= 190 | github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= 191 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 192 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 193 | github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= 194 | github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= 195 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 196 | github.com/imkira/go-interpol v1.0.0 h1:HrmLyvOLJyjR0YofMw8QGdCIuYOs4TJUBDNU5sJC09E= 197 | github.com/imkira/go-interpol v1.0.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 198 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 199 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 200 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 201 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 202 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 203 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 204 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 205 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 206 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 207 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 208 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 209 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 210 | github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= 211 | github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4= 212 | github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= 213 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 214 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 215 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 216 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 217 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 218 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 219 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 220 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 221 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 222 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 223 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 224 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 225 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 226 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 227 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 228 | github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= 229 | github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 230 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 231 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 232 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 233 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 234 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 235 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 236 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 237 | github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= 238 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 239 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 240 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 241 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 242 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 243 | github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= 244 | github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= 245 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 246 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 247 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 248 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 249 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 250 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 251 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 252 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 253 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 254 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 255 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 256 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 257 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 258 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 259 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 260 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 261 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 262 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 263 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 264 | github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 265 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 266 | github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= 267 | github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= 268 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 269 | github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= 270 | github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= 271 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 272 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 273 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 274 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 275 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 276 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 277 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 278 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 279 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 280 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 281 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 282 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 283 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 284 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 285 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 286 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 287 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 288 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 289 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 290 | github.com/lmittmann/tint v1.0.2 h1:9XZ+JvEzjvd3VNVugYqo3j+dl0NRju8k9FquAusJExM= 291 | github.com/lmittmann/tint v1.0.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 292 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 293 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 294 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 295 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 296 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 297 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 298 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 299 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 300 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 301 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 302 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 303 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 304 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 305 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 306 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 307 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 308 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 309 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 310 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 311 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 312 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 313 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 314 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 315 | github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= 316 | github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= 317 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 318 | github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= 319 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 320 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 321 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 322 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 323 | github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 324 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 325 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 326 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 327 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 328 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 329 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 330 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 331 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 332 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 333 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 334 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 335 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 336 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 337 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 338 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 339 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 340 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 341 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 342 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 343 | github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= 344 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 345 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 346 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 347 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 348 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 349 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 350 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 351 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 352 | github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= 353 | github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= 354 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 355 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 356 | github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= 357 | github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 358 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 359 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 360 | github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= 361 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 362 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 363 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 364 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 365 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 366 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 367 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 368 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 369 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 370 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 371 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 372 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 373 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 374 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 375 | github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 376 | github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 377 | github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 378 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 379 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 380 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 381 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 382 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 383 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 384 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 385 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 386 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 387 | github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 388 | github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 389 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 390 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 391 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 392 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 393 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 394 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 395 | github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= 396 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 397 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 398 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 399 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 400 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 401 | github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= 402 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 403 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 404 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 405 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 406 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 407 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 408 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 409 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 410 | github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f h1:PgA+Olipyj258EIEYnpFFONrrCcAIWNUNoFhUfMqAGY= 411 | github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY= 412 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 413 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 414 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 415 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 416 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 417 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 418 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 419 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 420 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 421 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 422 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 423 | github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= 424 | github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 425 | github.com/snowzach/certtools v1.0.2 h1:C1XzUZZeIsJjk10/CMSBYmFTUtFSgq+dowsMjJhmsPE= 426 | github.com/snowzach/certtools v1.0.2/go.mod h1:rZqO5JtBTNCAEu184f6KqG1x3Hfg1KX3kVtY93L6xhQ= 427 | github.com/snowzach/golib v1.0.4 h1:VXZwwGvgUozjjmH3n5gaGRJlBjHWlQmMrjMpfY/N0Kw= 428 | github.com/snowzach/golib v1.0.4/go.mod h1:8DthOtBtE0/1trVmlQejFTeMKFSfZ/hUBFel9O2jZ58= 429 | github.com/snowzach/queryp v0.3.6 h1:5h/eVkJpT55RQexJbKkDjb0wPY5ff1xeOqFZ+mIM3S4= 430 | github.com/snowzach/queryp v0.3.6/go.mod h1:QCSTgQZ6VCEWv226gfo20NedrMJiuC2QazVghdmanto= 431 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 432 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 433 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 434 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 435 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 436 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 437 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 438 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 439 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 440 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 441 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 442 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 443 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 444 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 445 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 446 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 447 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 448 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 449 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 450 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 451 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 452 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 453 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 454 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 455 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 456 | github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= 457 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= 458 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 459 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 460 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 461 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 462 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 463 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 464 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 465 | github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= 466 | github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= 467 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= 468 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 469 | github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= 470 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 471 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= 472 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 473 | github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= 474 | github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= 475 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 476 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 477 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 478 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 479 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 480 | go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= 481 | go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= 482 | go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= 483 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 484 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 485 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 486 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 487 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 488 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 489 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 490 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 491 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 492 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 493 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 494 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 495 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 496 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 497 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 498 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 499 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 500 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 501 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 502 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 503 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 504 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= 505 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 506 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 507 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 508 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 509 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 510 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 511 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 512 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 513 | golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= 514 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 515 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 516 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 517 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 518 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 519 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 520 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 521 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 522 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 523 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 524 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 525 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 526 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 527 | golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 528 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 529 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 530 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 531 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 532 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 533 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 534 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 535 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 536 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 537 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 538 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 539 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 540 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 541 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 542 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 543 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 544 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 545 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 546 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 547 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 548 | golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 549 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 550 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 551 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 552 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 553 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 554 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 555 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 556 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 557 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 558 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 559 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 560 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 561 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 562 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 563 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 564 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 565 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 566 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 567 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 568 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 569 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 570 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 571 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 572 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 573 | golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 574 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 575 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 576 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 577 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 578 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 579 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 580 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 581 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 582 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 583 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 584 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 585 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 586 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 587 | golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 588 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 589 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 590 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 591 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 592 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 593 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 594 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 595 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 596 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 597 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 598 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 599 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 600 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 601 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 602 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 603 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 604 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 605 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 606 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 607 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 608 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 609 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 610 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 611 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 612 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 613 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 614 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 615 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 616 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 617 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 618 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 619 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 620 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 621 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 622 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 623 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 624 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 625 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 626 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 627 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 628 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 629 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 630 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 631 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 632 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 633 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 634 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 635 | golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 636 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 637 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 638 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 639 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 640 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 641 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 642 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 643 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 644 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 645 | golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= 646 | golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= 647 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 648 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 649 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 650 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 651 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 652 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 653 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 654 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 655 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 656 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 657 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 658 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 659 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 660 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 661 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 662 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 663 | google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 664 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 665 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 666 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 667 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 668 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 669 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 670 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 671 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 672 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 673 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 674 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 675 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 676 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 677 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 678 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 679 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 680 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 681 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 682 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 683 | gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= 684 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 685 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 686 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 687 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 688 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 689 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 690 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 691 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 692 | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 693 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 694 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 695 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 696 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 697 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 698 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 699 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 700 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 701 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 702 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 703 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 704 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 705 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 706 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 707 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 708 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 709 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 710 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 711 | moul.io/http2curl v1.0.1-0.20190925090545-5cd742060b0e h1:C7q+e9M5nggAvWfVg9Nl66kebKeuJlP3FD58V4RR5wo= 712 | moul.io/http2curl v1.0.1-0.20190925090545-5cd742060b0e/go.mod h1:nejbQVfXh96n9dSF6cH3Jsk/QI1Z2oEL7sSI2ifXFNA= 713 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 714 | -------------------------------------------------------------------------------- /gorestapi/gorestapi.go: -------------------------------------------------------------------------------- 1 | package gorestapi 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/snowzach/queryp" 7 | ) 8 | 9 | // GRStore is the persistent store of things 10 | type GRStore interface { 11 | ThingGetByID(ctx context.Context, id string) (*Thing, error) 12 | ThingSave(ctx context.Context, thing *Thing) error 13 | ThingDeleteByID(ctx context.Context, id string) error 14 | ThingsFind(ctx context.Context, qp *queryp.QueryParameters) ([]*Thing, *int64, error) 15 | 16 | WidgetGetByID(ctx context.Context, id string) (*Widget, error) 17 | WidgetSave(ctx context.Context, thing *Widget) error 18 | WidgetDeleteByID(ctx context.Context, id string) error 19 | WidgetsFind(ctx context.Context, qp *queryp.QueryParameters) ([]*Widget, *int64, error) 20 | } 21 | -------------------------------------------------------------------------------- /gorestapi/mainrpc/mainrpc.go: -------------------------------------------------------------------------------- 1 | package mainrpc 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/go-chi/chi/v5" 7 | "github.com/snowzach/golib/log" 8 | 9 | "github.com/snowzach/gorestapi/gorestapi" 10 | ) 11 | 12 | // Server is the API web server 13 | type Server struct { 14 | logger *slog.Logger 15 | router chi.Router 16 | grStore gorestapi.GRStore 17 | } 18 | 19 | // Setup will setup the API listener 20 | func Setup(router chi.Router, grStore gorestapi.GRStore) error { 21 | 22 | s := &Server{ 23 | logger: log.Logger.With("context", "mainrpc"), 24 | router: router, 25 | grStore: grStore, 26 | } 27 | 28 | // Base Functions 29 | s.router.Route("/api", func(r chi.Router) { 30 | r.Post("/things", s.ThingSave()) 31 | r.Get("/things/{id}", s.ThingGetByID()) 32 | r.Delete("/things/{id}", s.ThingDeleteByID()) 33 | r.Get("/things", s.ThingsFind()) 34 | 35 | r.Post("/widgets", s.WidgetSave()) 36 | r.Get("/widgets/{id}", s.WidgetGetByID()) 37 | r.Delete("/widgets/{id}", s.WidgetDeleteByID()) 38 | r.Get("/widgets", s.WidgetsFind()) 39 | }) 40 | 41 | return nil 42 | 43 | } 44 | -------------------------------------------------------------------------------- /gorestapi/mainrpc/thing.go: -------------------------------------------------------------------------------- 1 | package mainrpc 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | "github.com/go-chi/chi/v5/middleware" 8 | "github.com/snowzach/queryp" 9 | 10 | "github.com/snowzach/golib/httpserver/render" 11 | "github.com/snowzach/golib/store" 12 | "github.com/snowzach/gorestapi/gorestapi" 13 | ) 14 | 15 | // ThingSave saves a thing 16 | // 17 | // @ID ThingSave 18 | // @Tags Things 19 | // @Summary Save thing 20 | // @Description Save a thing 21 | // @Accept json 22 | // @Produce json 23 | // @Param thing body gorestapi.ThingExample true "Thing" 24 | // @Success 200 {object} gorestapi.Thing 25 | // @Failure 400 {object} render.ErrResponse "Invalid Argument" 26 | // @Failure 500 {object} render.ErrResponse "Internal Error" 27 | // @Router /things [post] 28 | func (s *Server) ThingSave() http.HandlerFunc { 29 | 30 | return func(w http.ResponseWriter, r *http.Request) { 31 | 32 | ctx := r.Context() 33 | 34 | var thing = new(gorestapi.Thing) 35 | if err := render.DecodeJSON(r.Body, thing); err != nil { 36 | render.ErrInvalidRequest(w, err) 37 | return 38 | } 39 | 40 | err := s.grStore.ThingSave(ctx, thing) 41 | if err != nil { 42 | if serr, ok := err.(*store.Error); ok { 43 | render.ErrInvalidRequest(w, serr.ErrorForOp(store.ErrorOpSave)) 44 | } else { 45 | requestID := middleware.GetReqID(ctx) 46 | render.ErrInternalWithID(w, requestID, nil) 47 | s.logger.Error("ThingSave error", "error", err, "request_id", requestID) 48 | } 49 | return 50 | } 51 | 52 | render.JSON(w, http.StatusOK, thing) 53 | } 54 | 55 | } 56 | 57 | // ThingGetByID saves a thing 58 | // 59 | // @ID ThingGetByID 60 | // @Tags Things 61 | // @Summary Get thing 62 | // @Description Get a thing 63 | // @Param id path string true "ID" 64 | // @Accept json 65 | // @Produce json 66 | // @Success 200 {object} gorestapi.Thing 67 | // @Failure 400 {object} render.ErrResponse "Invalid Argument" 68 | // @Failure 404 {object} render.ErrResponse "Not Found" 69 | // @Failure 500 {object} render.ErrResponse "Internal Error" 70 | // @Router /things/{id} [get] 71 | func (s *Server) ThingGetByID() http.HandlerFunc { 72 | return func(w http.ResponseWriter, r *http.Request) { 73 | 74 | ctx := r.Context() 75 | 76 | id := chi.URLParam(r, "id") 77 | 78 | thing, err := s.grStore.ThingGetByID(ctx, id) 79 | if err != nil { 80 | if err == store.ErrNotFound { 81 | render.ErrResourceNotFound(w, "thing") 82 | } else if serr, ok := err.(*store.Error); ok { 83 | render.ErrInvalidRequest(w, serr.ErrorForOp(store.ErrorOpGet)) 84 | } else { 85 | requestID := middleware.GetReqID(ctx) 86 | render.ErrInternalWithID(w, requestID, nil) 87 | s.logger.Error("ThingGetByID error", "error", err, "request_id", requestID) 88 | } 89 | return 90 | } 91 | 92 | render.JSON(w, http.StatusOK, thing) 93 | } 94 | 95 | } 96 | 97 | // ThingDeleteByID saves a thing 98 | // 99 | // @ID ThingDeleteByID 100 | // @Tags Things 101 | // @Summary Delete thing 102 | // @Description Delete a thing 103 | // @Accept json 104 | // @Produce json 105 | // @Param id path string true "ID" 106 | // @Success 204 "Success" 107 | // @Failure 400 {object} render.ErrResponse "Invalid Argument" 108 | // @Failure 404 {object} render.ErrResponse "Not Found" 109 | // @Failure 500 {object} render.ErrResponse "Internal Error" 110 | // @Router /things/{id} [delete] 111 | func (s *Server) ThingDeleteByID() http.HandlerFunc { 112 | return func(w http.ResponseWriter, r *http.Request) { 113 | 114 | ctx := r.Context() 115 | 116 | id := chi.URLParam(r, "id") 117 | 118 | err := s.grStore.ThingDeleteByID(ctx, id) 119 | if err != nil { 120 | if err == store.ErrNotFound { 121 | render.ErrResourceNotFound(w, "thing") 122 | } else if serr, ok := err.(*store.Error); ok { 123 | render.ErrInvalidRequest(w, serr.ErrorForOp(store.ErrorOpDelete)) 124 | } else { 125 | requestID := middleware.GetReqID(ctx) 126 | render.ErrInternalWithID(w, requestID, nil) 127 | s.logger.Error("ThingDeleteByID error", "error", err, "request_id", requestID) 128 | } 129 | return 130 | } 131 | 132 | render.NoContent(w) 133 | 134 | } 135 | 136 | } 137 | 138 | // ThingsFind saves a thing 139 | // 140 | // @ID ThingsFind 141 | // @Tags Things 142 | // @Summary Find things 143 | // @Description Find things 144 | // @Accept json 145 | // @Produce json 146 | // @Param id query string false "id" 147 | // @Param name query string false "name" 148 | // @Param description query string false "description" 149 | // @Param offset query int false "offset" 150 | // @Param limit query int false "limit" 151 | // @Param sort query string false "query" 152 | // @Success 200 {array} gorestapi.Thing 153 | // @Failure 400 {object} render.ErrResponse "Invalid Argument" 154 | // @Failure 500 {object} render.ErrResponse "Internal Error" 155 | // @Router /things [get] 156 | func (s *Server) ThingsFind() http.HandlerFunc { 157 | return func(w http.ResponseWriter, r *http.Request) { 158 | 159 | ctx := r.Context() 160 | 161 | qp, err := queryp.ParseRawQuery(r.URL.RawQuery) 162 | if err != nil { 163 | render.ErrInvalidRequest(w, err) 164 | return 165 | } 166 | 167 | things, count, err := s.grStore.ThingsFind(ctx, qp) 168 | if err != nil { 169 | if serr, ok := err.(*store.Error); ok { 170 | render.ErrInvalidRequest(w, serr.ErrorForOp(store.ErrorOpFind)) 171 | } else { 172 | requestID := middleware.GetReqID(ctx) 173 | render.ErrInternalWithID(w, requestID, nil) 174 | s.logger.Error("ThingsFind error", "error", err, "request_id", requestID) 175 | } 176 | return 177 | } 178 | 179 | render.JSON(w, http.StatusOK, store.Results{Count: count, Results: things}) 180 | 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /gorestapi/mainrpc/thing_test.go: -------------------------------------------------------------------------------- 1 | package mainrpc 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gavv/httpexpect/v2" 9 | "github.com/go-chi/chi/v5" 10 | "github.com/snowzach/golib/store" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | 14 | "github.com/snowzach/gorestapi/gorestapi" 15 | "github.com/snowzach/gorestapi/mocks" 16 | ) 17 | 18 | func TestThingPost(t *testing.T) { 19 | 20 | // Create test server 21 | r := chi.NewRouter() 22 | server := httptest.NewServer(r) 23 | defer server.Close() 24 | 25 | // Mock Store and server 26 | grs := new(mocks.GRStore) 27 | err := Setup(r, grs) 28 | assert.Nil(t, err) 29 | 30 | // Create Item 31 | i := &gorestapi.Thing{ 32 | ID: "id", 33 | Name: "name", 34 | } 35 | 36 | // Mock call to item store 37 | grs.On("ThingSave", mock.Anything, i).Once().Return(nil) 38 | 39 | // Make request and validate we get back proper response 40 | e := httpexpect.New(t, server.URL) 41 | e.POST("/api/things").WithJSON(i).Expect().Status(http.StatusOK).JSON().Object().Equal(i) 42 | 43 | // Check remaining expectations 44 | grs.AssertExpectations(t) 45 | 46 | } 47 | 48 | func TestThingsFind(t *testing.T) { 49 | 50 | // Create test server 51 | r := chi.NewRouter() 52 | server := httptest.NewServer(r) 53 | defer server.Close() 54 | 55 | // Mock Store and server 56 | grs := new(mocks.GRStore) 57 | err := Setup(r, grs) 58 | assert.Nil(t, err) 59 | 60 | // Return Item 61 | i := []*gorestapi.Thing{ 62 | { 63 | ID: "id1", 64 | Name: "name1", 65 | }, 66 | { 67 | ID: "id2", 68 | Name: "name2", 69 | }, 70 | } 71 | var count int64 = 2 72 | 73 | // Mock call to item store 74 | grs.On("ThingsFind", mock.Anything, mock.AnythingOfType("*queryp.QueryParameters")).Once().Return(i, &count, nil) 75 | 76 | // Make request and validate we get back proper response 77 | e := httpexpect.New(t, server.URL) 78 | e.GET("/api/things").Expect().Status(http.StatusOK).JSON().Object().Equal(&store.Results{Count: &count, Results: i}) 79 | 80 | // Check remaining expectations 81 | grs.AssertExpectations(t) 82 | 83 | } 84 | 85 | func TestThingGetByID(t *testing.T) { 86 | 87 | // Create test server 88 | r := chi.NewRouter() 89 | server := httptest.NewServer(r) 90 | defer server.Close() 91 | 92 | // Mock Store and server 93 | grs := new(mocks.GRStore) 94 | err := Setup(r, grs) 95 | assert.Nil(t, err) 96 | 97 | // Create Item 98 | i := &gorestapi.Thing{ 99 | ID: "id", 100 | Name: "name", 101 | } 102 | 103 | // Mock call to item store 104 | grs.On("ThingGetByID", mock.Anything, "1234").Once().Return(i, nil) 105 | 106 | // Make request and validate we get back proper response 107 | e := httpexpect.New(t, server.URL) 108 | e.GET("/api/things/1234").Expect().Status(http.StatusOK).JSON().Object().Equal(&i) 109 | 110 | // Check remaining expectations 111 | grs.AssertExpectations(t) 112 | 113 | } 114 | 115 | func TestThingDeleteByID(t *testing.T) { 116 | 117 | // Create test server 118 | r := chi.NewRouter() 119 | server := httptest.NewServer(r) 120 | defer server.Close() 121 | 122 | // Mock Store and server 123 | grs := new(mocks.GRStore) 124 | err := Setup(r, grs) 125 | assert.Nil(t, err) 126 | 127 | // Mock call to item store 128 | grs.On("ThingDeleteByID", mock.Anything, "1234").Once().Return(nil) 129 | 130 | // Make request and validate we get back proper response 131 | e := httpexpect.New(t, server.URL) 132 | e.DELETE("/api/things/1234").Expect().Status(http.StatusNoContent) 133 | 134 | // Check remaining expectations 135 | grs.AssertExpectations(t) 136 | 137 | } 138 | -------------------------------------------------------------------------------- /gorestapi/mainrpc/widget.go: -------------------------------------------------------------------------------- 1 | package mainrpc 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | "github.com/go-chi/chi/v5/middleware" 8 | "github.com/snowzach/queryp" 9 | 10 | "github.com/snowzach/golib/httpserver/render" 11 | "github.com/snowzach/golib/store" 12 | "github.com/snowzach/gorestapi/gorestapi" 13 | ) 14 | 15 | // WidgetSave saves a widget 16 | // 17 | // @ID WidgetSave 18 | // @Tags Widgets 19 | // @Summary Save widget 20 | // @Description Save a widget 21 | // @Accept json 22 | // @Produce json 23 | // @Param widget body gorestapi.WidgetExample true "Widget" 24 | // @Success 200 {object} gorestapi.Widget 25 | // @Failure 400 {object} render.ErrResponse "Invalid Argument" 26 | // @Failure 500 {object} render.ErrResponse "Internal Error" 27 | // @Router /widgets [post] 28 | func (s *Server) WidgetSave() http.HandlerFunc { 29 | 30 | return func(w http.ResponseWriter, r *http.Request) { 31 | 32 | ctx := r.Context() 33 | 34 | var widget = new(gorestapi.Widget) 35 | if err := render.DecodeJSON(r.Body, widget); err != nil { 36 | render.ErrInvalidRequest(w, err) 37 | return 38 | } 39 | 40 | err := s.grStore.WidgetSave(ctx, widget) 41 | if err != nil { 42 | if serr, ok := err.(*store.Error); ok { 43 | render.ErrInvalidRequest(w, serr.ErrorForOp(store.ErrorOpSave)) 44 | } else { 45 | requestID := middleware.GetReqID(ctx) 46 | render.ErrInternalWithID(w, requestID, nil) 47 | s.logger.Error("WidgetSave error", "error", err, "request_id", requestID) 48 | } 49 | return 50 | } 51 | 52 | render.JSON(w, http.StatusOK, widget) 53 | } 54 | 55 | } 56 | 57 | // WidgetGetByID saves a widget 58 | // 59 | // @ID WidgetGetByID 60 | // @Tags Widgets 61 | // @Summary Get widget 62 | // @Description Get a widget 63 | // @Accept json 64 | // @Produce json 65 | // @Param id path string true "ID" 66 | // @Success 200 {object} gorestapi.Widget 67 | // @Failure 400 {object} render.ErrResponse "Invalid Argument" 68 | // @Failure 404 {object} render.ErrResponse "Not Found" 69 | // @Failure 500 {object} render.ErrResponse "Internal Error" 70 | // @Router /widgets/{id} [get] 71 | func (s *Server) WidgetGetByID() http.HandlerFunc { 72 | return func(w http.ResponseWriter, r *http.Request) { 73 | 74 | ctx := r.Context() 75 | 76 | id := chi.URLParam(r, "id") 77 | 78 | widget, err := s.grStore.WidgetGetByID(ctx, id) 79 | if err != nil { 80 | if err == store.ErrNotFound { 81 | render.ErrResourceNotFound(w, "widget") 82 | } else if serr, ok := err.(*store.Error); ok { 83 | render.ErrInvalidRequest(w, serr.ErrorForOp(store.ErrorOpGet)) 84 | } else { 85 | requestID := middleware.GetReqID(ctx) 86 | render.ErrInternalWithID(w, requestID, nil) 87 | s.logger.Error("WidgetGetByID error", "error", err, "request_id", requestID) 88 | } 89 | return 90 | } 91 | 92 | render.JSON(w, http.StatusOK, widget) 93 | } 94 | } 95 | 96 | // WidgetDeleteByID saves a widget 97 | // 98 | // @ID WidgetDeleteByID 99 | // @Tags Widgets 100 | // @Summary Delete widget 101 | // @Description Delete a widget 102 | // @Accept json 103 | // @Produce json 104 | // @Param id path string true "ID" 105 | // @Success 204 "Success" 106 | // @Failure 400 {object} render.ErrResponse "Invalid Argument" 107 | // @Failure 404 {object} render.ErrResponse "Not Found" 108 | // @Failure 500 {object} render.ErrResponse "Internal Error" 109 | // @Router /widgets/{id} [delete] 110 | func (s *Server) WidgetDeleteByID() http.HandlerFunc { 111 | return func(w http.ResponseWriter, r *http.Request) { 112 | 113 | ctx := r.Context() 114 | 115 | id := chi.URLParam(r, "id") 116 | 117 | err := s.grStore.WidgetDeleteByID(ctx, id) 118 | if err != nil { 119 | if err == store.ErrNotFound { 120 | render.ErrResourceNotFound(w, "widget") 121 | } else if serr, ok := err.(*store.Error); ok { 122 | render.ErrInvalidRequest(w, serr.ErrorForOp(store.ErrorOpDelete)) 123 | } else { 124 | requestID := middleware.GetReqID(ctx) 125 | render.ErrInternalWithID(w, requestID, nil) 126 | s.logger.Error("WidgetDeleteByID error", "error", err, "request_id", requestID) 127 | } 128 | return 129 | } 130 | 131 | render.NoContent(w) 132 | } 133 | } 134 | 135 | // WidgetsFind saves a widget 136 | // 137 | // @ID WidgetsFind 138 | // @Tags Widgets 139 | // @Summary Find widgets 140 | // @Description Find widgets 141 | // @Accept json 142 | // @Produce json 143 | // @Param id query string false "id" 144 | // @Param name query string false "name" 145 | // @Param description query string false "description" 146 | // @Param offset query int false "offset" 147 | // @Param limit query int false "limit" 148 | // @Param sort query string false "query" 149 | // @Success 200 {array} gorestapi.Widget 150 | // @Failure 400 {object} render.ErrResponse "Invalid Argument" 151 | // @Failure 500 {object} render.ErrResponse "Internal Error" 152 | // @Router /widgets [get] 153 | func (s *Server) WidgetsFind() http.HandlerFunc { 154 | return func(w http.ResponseWriter, r *http.Request) { 155 | 156 | ctx := r.Context() 157 | 158 | qp, err := queryp.ParseRawQuery(r.URL.RawQuery) 159 | if err != nil { 160 | render.ErrInvalidRequest(w, err) 161 | return 162 | } 163 | 164 | widgets, count, err := s.grStore.WidgetsFind(ctx, qp) 165 | if err != nil { 166 | if serr, ok := err.(*store.Error); ok { 167 | render.ErrInvalidRequest(w, serr.ErrorForOp(store.ErrorOpFind)) 168 | } else { 169 | requestID := middleware.GetReqID(ctx) 170 | render.ErrInternalWithID(w, requestID, nil) 171 | s.logger.Error("WidgetsFind error", "error", err, "request_id", requestID) 172 | } 173 | return 174 | } 175 | 176 | render.JSON(w, http.StatusOK, store.Results{Count: count, Results: widgets}) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /gorestapi/mainrpc/widget_test.go: -------------------------------------------------------------------------------- 1 | package mainrpc 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gavv/httpexpect/v2" 9 | "github.com/go-chi/chi/v5" 10 | "github.com/snowzach/golib/store" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | 14 | "github.com/snowzach/gorestapi/gorestapi" 15 | "github.com/snowzach/gorestapi/mocks" 16 | ) 17 | 18 | func TestWidgetPost(t *testing.T) { 19 | 20 | // Create test server 21 | r := chi.NewRouter() 22 | server := httptest.NewServer(r) 23 | defer server.Close() 24 | 25 | // Mock Store and server 26 | grs := new(mocks.GRStore) 27 | err := Setup(r, grs) 28 | assert.Nil(t, err) 29 | 30 | // Create Item 31 | i := &gorestapi.Widget{ 32 | ID: "id", 33 | Name: "name", 34 | } 35 | 36 | // Mock call to item store 37 | grs.On("WidgetSave", mock.Anything, i).Once().Return(nil) 38 | 39 | // Make request and validate we get back proper response 40 | e := httpexpect.New(t, server.URL) 41 | e.POST("/api/widgets").WithJSON(i).Expect().Status(http.StatusOK).JSON().Object().Equal(i) 42 | 43 | // Check remaining expectations 44 | grs.AssertExpectations(t) 45 | 46 | } 47 | 48 | func TestWidgetsFind(t *testing.T) { 49 | 50 | // Create test server 51 | r := chi.NewRouter() 52 | server := httptest.NewServer(r) 53 | defer server.Close() 54 | 55 | // Mock Store and server 56 | grs := new(mocks.GRStore) 57 | err := Setup(r, grs) 58 | assert.Nil(t, err) 59 | 60 | // Return Item 61 | i := []*gorestapi.Widget{ 62 | &gorestapi.Widget{ 63 | ID: "id1", 64 | Name: "name1", 65 | }, 66 | &gorestapi.Widget{ 67 | ID: "id2", 68 | Name: "name2", 69 | }, 70 | } 71 | var count int64 = 2 72 | 73 | // Mock call to item store 74 | grs.On("WidgetsFind", mock.Anything, mock.AnythingOfType("*queryp.QueryParameters")).Once().Return(i, &count, nil) 75 | 76 | // Make request and validate we get back proper response 77 | e := httpexpect.New(t, server.URL) 78 | e.GET("/api/widgets").Expect().Status(http.StatusOK).JSON().Object().Equal(&store.Results{Count: &count, Results: i}) 79 | 80 | // Check remaining expectations 81 | grs.AssertExpectations(t) 82 | 83 | } 84 | 85 | func TestWidgetGetByID(t *testing.T) { 86 | 87 | // Create test server 88 | r := chi.NewRouter() 89 | server := httptest.NewServer(r) 90 | defer server.Close() 91 | 92 | // Mock Store and server 93 | grs := new(mocks.GRStore) 94 | err := Setup(r, grs) 95 | assert.Nil(t, err) 96 | 97 | // Create Item 98 | i := &gorestapi.Widget{ 99 | ID: "id", 100 | Name: "name", 101 | } 102 | 103 | // Mock call to item store 104 | grs.On("WidgetGetByID", mock.Anything, "1234").Once().Return(i, nil) 105 | 106 | // Make request and validate we get back proper response 107 | e := httpexpect.New(t, server.URL) 108 | e.GET("/api/widgets/1234").Expect().Status(http.StatusOK).JSON().Object().Equal(&i) 109 | 110 | // Check remaining expectations 111 | grs.AssertExpectations(t) 112 | 113 | } 114 | 115 | func TestWidgetDeleteByID(t *testing.T) { 116 | 117 | // Create test server 118 | r := chi.NewRouter() 119 | server := httptest.NewServer(r) 120 | defer server.Close() 121 | 122 | // Mock Store and server 123 | grs := new(mocks.GRStore) 124 | err := Setup(r, grs) 125 | assert.Nil(t, err) 126 | 127 | // Mock call to item store 128 | grs.On("WidgetDeleteByID", mock.Anything, "1234").Once().Return(nil) 129 | 130 | // Make request and validate we get back proper response 131 | e := httpexpect.New(t, server.URL) 132 | e.DELETE("/api/widgets/1234").Expect().Status(http.StatusNoContent) 133 | 134 | // Check remaining expectations 135 | grs.AssertExpectations(t) 136 | 137 | } 138 | -------------------------------------------------------------------------------- /gorestapi/swagger.go: -------------------------------------------------------------------------------- 1 | package gorestapi 2 | 3 | // @title Swagger Example API 4 | // @version 1.0 5 | // @description This is a sample server celler server. 6 | // @termsOfService http://swagger.io/terms/ 7 | 8 | // @contact.name API Support 9 | // @contact.url http://www.swagger.io/support 10 | // @contact.email support@swagger.io 11 | 12 | // @license.name Apache 2.0 13 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 14 | 15 | // @host localhost:8080 16 | // @BasePath /api/v1 17 | // @query.collection.format multi 18 | 19 | // @securityDefinitions.basic BasicAuth 20 | 21 | // @securityDefinitions.apikey ApiKeyAuth 22 | // @in header 23 | // @name Authorization 24 | 25 | // @securitydefinitions.oauth2.application OAuth2Application 26 | // @tokenUrl https://example.com/oauth/token 27 | // @scope.write Grants write access 28 | // @scope.admin Grants read and write access to administrative information 29 | 30 | // @securitydefinitions.oauth2.implicit OAuth2Implicit 31 | // @authorizationurl https://example.com/oauth/authorize 32 | // @scope.write Grants write access 33 | // @scope.admin Grants read and write access to administrative information 34 | 35 | // @securitydefinitions.oauth2.password OAuth2Password 36 | // @tokenUrl https://example.com/oauth/token 37 | // @scope.read Grants read access 38 | // @scope.write Grants write access 39 | // @scope.admin Grants read and write access to administrative information 40 | 41 | // @securitydefinitions.oauth2.accessCode OAuth2AccessCode 42 | // @tokenUrl https://example.com/oauth/token 43 | // @authorizationurl https://example.com/oauth/authorize 44 | // @scope.admin Grants read and write access to administrative information 45 | 46 | // @x-extension-openapi {"example": "value on a json format"} 47 | -------------------------------------------------------------------------------- /gorestapi/thing.go: -------------------------------------------------------------------------------- 1 | package gorestapi 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // Thing 9 | type Thing struct { 10 | // ID (Auto-Generated) 11 | ID string `json:"id"` 12 | // Created Timestamp 13 | Created time.Time `json:"created,omitempty"` 14 | // Updated Timestamp 15 | Updated time.Time `json:"updated,omitempty"` 16 | // Name 17 | Name string `json:"name"` 18 | // Description 19 | Description string `json:"description"` 20 | } 21 | 22 | // ThingExample 23 | type ThingExample struct { 24 | // Name 25 | Name string `json:"name"` 26 | // Description 27 | Description string `json:"description"` 28 | } 29 | 30 | // String is the stringer method 31 | func (t *Thing) String() string { 32 | b, _ := json.Marshal(t) 33 | return string(b) 34 | } 35 | -------------------------------------------------------------------------------- /gorestapi/thing_test.go: -------------------------------------------------------------------------------- 1 | package gorestapi 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestThingString(t *testing.T) { 12 | thing := &Thing{ 13 | ID: "id1", 14 | Created: time.Now(), 15 | Updated: time.Now().Add(time.Minute), 16 | Name: "name1", 17 | Description: "description1", 18 | } 19 | 20 | b, err := json.Marshal(thing) 21 | assert.Nil(t, err) 22 | 23 | assert.Equal(t, string(b), thing.String()) 24 | } 25 | -------------------------------------------------------------------------------- /gorestapi/widget.go: -------------------------------------------------------------------------------- 1 | package gorestapi 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // Widget 9 | // swagger:model gorestapi_Widget 10 | type Widget struct { 11 | // ID (Auto-Generated) 12 | ID string `json:"id"` 13 | // Created Timestamp 14 | Created time.Time `json:"created,omitempty"` 15 | // Updated Timestamp 16 | Updated time.Time `json:"updated,omitempty"` 17 | // Name 18 | Name string `json:"name"` 19 | // Description 20 | Description string `json:"description"` 21 | // ThingID 22 | ThingID *string `json:"thing_id,omitempty" db:"thing_id"` 23 | 24 | // Loaded Structs 25 | Thing *Thing `json:"thing,omitempty" db:"thing"` 26 | } 27 | 28 | // WidgetExample 29 | // swagger:model gorestapi_WidgetExample 30 | type WidgetExample struct { 31 | // Name 32 | Name string `json:"name"` 33 | // Description 34 | Description string `json:"description"` 35 | // ThingID 36 | ThingID *string `json:"thing_id,omitempty" db:"thing_id"` 37 | } 38 | 39 | // String is the stringer method 40 | func (w *Widget) String() string { 41 | b, _ := json.Marshal(w) 42 | return string(b) 43 | } 44 | 45 | // SyncDB will fix Loaded Structs 46 | func (w *Widget) SyncDB() { 47 | if w.ThingID == nil { 48 | w.Thing = nil 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gorestapi/widget_test.go: -------------------------------------------------------------------------------- 1 | package gorestapi 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestWidgetString(t *testing.T) { 12 | 13 | thingID := "tid1" 14 | 15 | widget := &Widget{ 16 | ID: "id1", 17 | Created: time.Now(), 18 | Updated: time.Now().Add(time.Minute), 19 | Name: "name1", 20 | Description: "description1", 21 | ThingID: &thingID, 22 | } 23 | 24 | b, err := json.Marshal(widget) 25 | assert.Nil(t, err) 26 | 27 | assert.Equal(t, string(b), widget.String()) 28 | } 29 | -------------------------------------------------------------------------------- /insomnia-collection.yaml: -------------------------------------------------------------------------------- 1 | _type: export 2 | __export_format: 4 3 | __export_date: 2023-08-11T14:02:50.299Z 4 | __export_source: insomnia.desktop.app:v2023.1.0 5 | resources: 6 | - _id: req_3c936d4a7c354c379ee68c042c52c0fa 7 | parentId: fld_b389c8759e0b4d2bb537a5f9389e5caa 8 | modified: 1691762291164 9 | created: 1675373848534 10 | url: "{{ _.base_url }}/api/things" 11 | name: GET /api/things 12 | description: "" 13 | method: GET 14 | body: 15 | mimeType: application/json 16 | text: "" 17 | parameters: [] 18 | headers: 19 | - name: Content-Type 20 | value: application/json 21 | authentication: {} 22 | metaSortKey: -1675373848534 23 | isPrivate: false 24 | settingStoreCookies: true 25 | settingSendCookies: true 26 | settingDisableRenderRequestBody: false 27 | settingEncodeUrl: true 28 | settingRebuildPath: true 29 | settingFollowRedirects: global 30 | _type: request 31 | - _id: fld_b389c8759e0b4d2bb537a5f9389e5caa 32 | parentId: wrk_b30b21c8133d424ba8c021f7807ed3ee 33 | modified: 1691762288830 34 | created: 1691762288830 35 | name: Things 36 | description: "" 37 | environment: {} 38 | environmentPropertyOrder: null 39 | metaSortKey: -1691762288831 40 | _type: request_group 41 | - _id: wrk_b30b21c8133d424ba8c021f7807ed3ee 42 | parentId: null 43 | modified: 1675373845729 44 | created: 1675373845729 45 | name: GoRestAPI 46 | description: "" 47 | scope: collection 48 | _type: workspace 49 | - _id: req_f58a987b48434c02a8342ad508ee3870 50 | parentId: fld_b389c8759e0b4d2bb537a5f9389e5caa 51 | modified: 1691762292835 52 | created: 1691761384939 53 | url: "{{ _.base_url }}/api/things/cjaqdgm8fv0a259ek4rg" 54 | name: GET /api/things/ 55 | description: "" 56 | method: GET 57 | body: 58 | mimeType: application/json 59 | text: "" 60 | parameters: [] 61 | headers: 62 | - name: Content-Type 63 | value: application/json 64 | authentication: {} 65 | metaSortKey: -1675373848484 66 | isPrivate: false 67 | settingStoreCookies: true 68 | settingSendCookies: true 69 | settingDisableRenderRequestBody: false 70 | settingEncodeUrl: true 71 | settingRebuildPath: true 72 | settingFollowRedirects: global 73 | _type: request 74 | - _id: req_c2360009daee4b11b84268968d210510 75 | parentId: fld_b389c8759e0b4d2bb537a5f9389e5caa 76 | modified: 1691762294274 77 | created: 1691761260024 78 | url: "{{ _.base_url }}/api/things" 79 | name: POST /api/things 80 | description: "" 81 | method: POST 82 | body: 83 | mimeType: application/json 84 | text: |- 85 | { 86 | "name": "Thing1", 87 | "description": "Thing1 Description" 88 | } 89 | parameters: [] 90 | headers: 91 | - name: Content-Type 92 | value: application/json 93 | authentication: {} 94 | metaSortKey: -1675373848434 95 | isPrivate: false 96 | settingStoreCookies: true 97 | settingSendCookies: true 98 | settingDisableRenderRequestBody: false 99 | settingEncodeUrl: true 100 | settingRebuildPath: true 101 | settingFollowRedirects: global 102 | _type: request 103 | - _id: req_0a086066d7ce437fa57fa6cf13f3ff53 104 | parentId: fld_b389c8759e0b4d2bb537a5f9389e5caa 105 | modified: 1691762295715 106 | created: 1691761294181 107 | url: "{{ _.base_url }}/api/things" 108 | name: POST /api/things (update) 109 | description: "" 110 | method: POST 111 | body: 112 | mimeType: application/json 113 | text: |- 114 | { 115 | "id": "cjaqdgm8fv0a259ek4rg", 116 | "name": "Thing1", 117 | "description": "Thing1 Description" 118 | } 119 | parameters: [] 120 | headers: 121 | - name: Content-Type 122 | value: application/json 123 | authentication: {} 124 | metaSortKey: -1675373848384 125 | isPrivate: false 126 | settingStoreCookies: true 127 | settingSendCookies: true 128 | settingDisableRenderRequestBody: false 129 | settingEncodeUrl: true 130 | settingRebuildPath: true 131 | settingFollowRedirects: global 132 | _type: request 133 | - _id: req_391839fba4184610861607feebd23b84 134 | parentId: fld_b389c8759e0b4d2bb537a5f9389e5caa 135 | modified: 1691762297464 136 | created: 1691761319699 137 | url: "{{ _.base_url }}/api/things/cjb3kim8fv0e9kr814d0" 138 | name: DELETE /api/things/ 139 | description: "" 140 | method: DELETE 141 | body: 142 | mimeType: application/json 143 | text: |- 144 | { 145 | "id": "cjaqdgm8fv0a259ek4rg", 146 | "name": "Thing1", 147 | "description": "Thing1 Description" 148 | } 149 | parameters: [] 150 | headers: 151 | - name: Content-Type 152 | value: application/json 153 | authentication: {} 154 | metaSortKey: -1675373848334 155 | isPrivate: false 156 | settingStoreCookies: true 157 | settingSendCookies: true 158 | settingDisableRenderRequestBody: false 159 | settingEncodeUrl: true 160 | settingRebuildPath: true 161 | settingFollowRedirects: global 162 | _type: request 163 | - _id: req_6bc39db3a9c54049a5fd1d93625d7798 164 | parentId: fld_0b9872b188274666949a9d707724f0d3 165 | modified: 1691762319170 166 | created: 1691761356613 167 | url: "{{ _.base_url }}/api/widgets" 168 | name: GET /api/widgets 169 | description: "" 170 | method: GET 171 | body: 172 | mimeType: application/json 173 | text: "" 174 | parameters: [] 175 | headers: 176 | - name: Content-Type 177 | value: application/json 178 | authentication: {} 179 | metaSortKey: -1660833432646.5625 180 | isPrivate: false 181 | settingStoreCookies: true 182 | settingSendCookies: true 183 | settingDisableRenderRequestBody: false 184 | settingEncodeUrl: true 185 | settingRebuildPath: true 186 | settingFollowRedirects: global 187 | _type: request 188 | - _id: fld_0b9872b188274666949a9d707724f0d3 189 | parentId: wrk_b30b21c8133d424ba8c021f7807ed3ee 190 | modified: 1691762314762 191 | created: 1691762303961 192 | name: Widgets 193 | description: "" 194 | environment: {} 195 | environmentPropertyOrder: null 196 | metaSortKey: -1660951647409.875 197 | _type: request_group 198 | - _id: req_2d318339585840da8ee79b5788bf5954 199 | parentId: fld_0b9872b188274666949a9d707724f0d3 200 | modified: 1691762320678 201 | created: 1691761708833 202 | url: "{{ _.base_url }}/api/widgets/cjaqev68fv0amf6hjvpg" 203 | name: GET /api/widgets/ 204 | description: "" 205 | method: GET 206 | body: 207 | mimeType: application/json 208 | text: "" 209 | parameters: [] 210 | headers: 211 | - name: Content-Type 212 | value: application/json 213 | authentication: {} 214 | metaSortKey: -1660833432596.5625 215 | isPrivate: false 216 | settingStoreCookies: true 217 | settingSendCookies: true 218 | settingDisableRenderRequestBody: false 219 | settingEncodeUrl: true 220 | settingRebuildPath: true 221 | settingFollowRedirects: global 222 | _type: request 223 | - _id: req_27f09c2e27b54635b7717586f11b2295 224 | parentId: fld_0b9872b188274666949a9d707724f0d3 225 | modified: 1691762322017 226 | created: 1691761725147 227 | url: "{{ _.base_url }}/api/widgets" 228 | name: POST /api/widgets 229 | description: "" 230 | method: POST 231 | body: 232 | mimeType: application/json 233 | text: |- 234 | { 235 | "name": "Widget2", 236 | "description": "Widget2 Description3" 237 | } 238 | parameters: [] 239 | headers: 240 | - name: Content-Type 241 | value: application/json 242 | authentication: {} 243 | metaSortKey: -1660833432546.5625 244 | isPrivate: false 245 | settingStoreCookies: true 246 | settingSendCookies: true 247 | settingDisableRenderRequestBody: false 248 | settingEncodeUrl: true 249 | settingRebuildPath: true 250 | settingFollowRedirects: global 251 | _type: request 252 | - _id: req_446edc5156fb4df2960de5015f478888 253 | parentId: fld_0b9872b188274666949a9d707724f0d3 254 | modified: 1691762323252 255 | created: 1691762022205 256 | url: "{{ _.base_url }}/api/widgets" 257 | name: POST /api/widgets (update) 258 | description: "" 259 | method: POST 260 | body: 261 | mimeType: application/json 262 | text: |- 263 | { 264 | "id": "cjb3qvm8fv08drou14l0", 265 | "name": "Widget2 Updated", 266 | "description": "Widget2 Description Updated" 267 | } 268 | parameters: [] 269 | headers: 270 | - name: Content-Type 271 | value: application/json 272 | authentication: {} 273 | metaSortKey: -1660833432496.5625 274 | isPrivate: false 275 | settingStoreCookies: true 276 | settingSendCookies: true 277 | settingDisableRenderRequestBody: false 278 | settingEncodeUrl: true 279 | settingRebuildPath: true 280 | settingFollowRedirects: global 281 | _type: request 282 | - _id: req_0c57600f3add40c984df504039a05318 283 | parentId: fld_0b9872b188274666949a9d707724f0d3 284 | modified: 1691762324570 285 | created: 1691762201175 286 | url: "{{ _.base_url }}/api/widgets/cjb3qvm8fv08drou14l0" 287 | name: DELETE /api/widgets/ 288 | description: "" 289 | method: DELETE 290 | body: 291 | mimeType: application/json 292 | text: "" 293 | parameters: [] 294 | headers: 295 | - name: Content-Type 296 | value: application/json 297 | authentication: {} 298 | metaSortKey: -1660833432446.5625 299 | isPrivate: false 300 | settingStoreCookies: true 301 | settingSendCookies: true 302 | settingDisableRenderRequestBody: false 303 | settingEncodeUrl: true 304 | settingRebuildPath: true 305 | settingFollowRedirects: global 306 | _type: request 307 | - _id: env_8bb83d7a8c298625d88d3598fd3320994f705a5a 308 | parentId: wrk_b30b21c8133d424ba8c021f7807ed3ee 309 | modified: 1691761197405 310 | created: 1675373845732 311 | name: Base Environment 312 | data: 313 | base_url: http://localhost:8080 314 | dataPropertyOrder: 315 | "&": 316 | - base_url 317 | color: null 318 | isPrivate: false 319 | metaSortKey: 1675373845732 320 | _type: environment 321 | - _id: jar_8bb83d7a8c298625d88d3598fd3320994f705a5a 322 | parentId: wrk_b30b21c8133d424ba8c021f7807ed3ee 323 | modified: 1675373845733 324 | created: 1675373845733 325 | name: Default Jar 326 | cookies: [] 327 | _type: cookie_jar 328 | - _id: spc_847d73816244482e9e354ac2d73d50ca 329 | parentId: wrk_b30b21c8133d424ba8c021f7807ed3ee 330 | modified: 1675373845729 331 | created: 1675373845729 332 | fileName: GoRestAPI 333 | contents: "" 334 | contentType: yaml 335 | _type: api_spec 336 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/snowzach/gorestapi/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /mocks/GRStore.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.20.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | gorestapi "github.com/snowzach/gorestapi/gorestapi" 9 | mock "github.com/stretchr/testify/mock" 10 | 11 | queryp "github.com/snowzach/queryp" 12 | ) 13 | 14 | // GRStore is an autogenerated mock type for the GRStore type 15 | type GRStore struct { 16 | mock.Mock 17 | } 18 | 19 | // ThingDeleteByID provides a mock function with given fields: ctx, id 20 | func (_m *GRStore) ThingDeleteByID(ctx context.Context, id string) error { 21 | ret := _m.Called(ctx, id) 22 | 23 | var r0 error 24 | if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { 25 | r0 = rf(ctx, id) 26 | } else { 27 | r0 = ret.Error(0) 28 | } 29 | 30 | return r0 31 | } 32 | 33 | // ThingGetByID provides a mock function with given fields: ctx, id 34 | func (_m *GRStore) ThingGetByID(ctx context.Context, id string) (*gorestapi.Thing, error) { 35 | ret := _m.Called(ctx, id) 36 | 37 | var r0 *gorestapi.Thing 38 | var r1 error 39 | if rf, ok := ret.Get(0).(func(context.Context, string) (*gorestapi.Thing, error)); ok { 40 | return rf(ctx, id) 41 | } 42 | if rf, ok := ret.Get(0).(func(context.Context, string) *gorestapi.Thing); ok { 43 | r0 = rf(ctx, id) 44 | } else { 45 | if ret.Get(0) != nil { 46 | r0 = ret.Get(0).(*gorestapi.Thing) 47 | } 48 | } 49 | 50 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 51 | r1 = rf(ctx, id) 52 | } else { 53 | r1 = ret.Error(1) 54 | } 55 | 56 | return r0, r1 57 | } 58 | 59 | // ThingSave provides a mock function with given fields: ctx, thing 60 | func (_m *GRStore) ThingSave(ctx context.Context, thing *gorestapi.Thing) error { 61 | ret := _m.Called(ctx, thing) 62 | 63 | var r0 error 64 | if rf, ok := ret.Get(0).(func(context.Context, *gorestapi.Thing) error); ok { 65 | r0 = rf(ctx, thing) 66 | } else { 67 | r0 = ret.Error(0) 68 | } 69 | 70 | return r0 71 | } 72 | 73 | // ThingsFind provides a mock function with given fields: ctx, qp 74 | func (_m *GRStore) ThingsFind(ctx context.Context, qp *queryp.QueryParameters) ([]*gorestapi.Thing, *int64, error) { 75 | ret := _m.Called(ctx, qp) 76 | 77 | var r0 []*gorestapi.Thing 78 | var r1 *int64 79 | var r2 error 80 | if rf, ok := ret.Get(0).(func(context.Context, *queryp.QueryParameters) ([]*gorestapi.Thing, *int64, error)); ok { 81 | return rf(ctx, qp) 82 | } 83 | if rf, ok := ret.Get(0).(func(context.Context, *queryp.QueryParameters) []*gorestapi.Thing); ok { 84 | r0 = rf(ctx, qp) 85 | } else { 86 | if ret.Get(0) != nil { 87 | r0 = ret.Get(0).([]*gorestapi.Thing) 88 | } 89 | } 90 | 91 | if rf, ok := ret.Get(1).(func(context.Context, *queryp.QueryParameters) *int64); ok { 92 | r1 = rf(ctx, qp) 93 | } else { 94 | if ret.Get(1) != nil { 95 | r1 = ret.Get(1).(*int64) 96 | } 97 | } 98 | 99 | if rf, ok := ret.Get(2).(func(context.Context, *queryp.QueryParameters) error); ok { 100 | r2 = rf(ctx, qp) 101 | } else { 102 | r2 = ret.Error(2) 103 | } 104 | 105 | return r0, r1, r2 106 | } 107 | 108 | // WidgetDeleteByID provides a mock function with given fields: ctx, id 109 | func (_m *GRStore) WidgetDeleteByID(ctx context.Context, id string) error { 110 | ret := _m.Called(ctx, id) 111 | 112 | var r0 error 113 | if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { 114 | r0 = rf(ctx, id) 115 | } else { 116 | r0 = ret.Error(0) 117 | } 118 | 119 | return r0 120 | } 121 | 122 | // WidgetGetByID provides a mock function with given fields: ctx, id 123 | func (_m *GRStore) WidgetGetByID(ctx context.Context, id string) (*gorestapi.Widget, error) { 124 | ret := _m.Called(ctx, id) 125 | 126 | var r0 *gorestapi.Widget 127 | var r1 error 128 | if rf, ok := ret.Get(0).(func(context.Context, string) (*gorestapi.Widget, error)); ok { 129 | return rf(ctx, id) 130 | } 131 | if rf, ok := ret.Get(0).(func(context.Context, string) *gorestapi.Widget); ok { 132 | r0 = rf(ctx, id) 133 | } else { 134 | if ret.Get(0) != nil { 135 | r0 = ret.Get(0).(*gorestapi.Widget) 136 | } 137 | } 138 | 139 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 140 | r1 = rf(ctx, id) 141 | } else { 142 | r1 = ret.Error(1) 143 | } 144 | 145 | return r0, r1 146 | } 147 | 148 | // WidgetSave provides a mock function with given fields: ctx, thing 149 | func (_m *GRStore) WidgetSave(ctx context.Context, thing *gorestapi.Widget) error { 150 | ret := _m.Called(ctx, thing) 151 | 152 | var r0 error 153 | if rf, ok := ret.Get(0).(func(context.Context, *gorestapi.Widget) error); ok { 154 | r0 = rf(ctx, thing) 155 | } else { 156 | r0 = ret.Error(0) 157 | } 158 | 159 | return r0 160 | } 161 | 162 | // WidgetsFind provides a mock function with given fields: ctx, qp 163 | func (_m *GRStore) WidgetsFind(ctx context.Context, qp *queryp.QueryParameters) ([]*gorestapi.Widget, *int64, error) { 164 | ret := _m.Called(ctx, qp) 165 | 166 | var r0 []*gorestapi.Widget 167 | var r1 *int64 168 | var r2 error 169 | if rf, ok := ret.Get(0).(func(context.Context, *queryp.QueryParameters) ([]*gorestapi.Widget, *int64, error)); ok { 170 | return rf(ctx, qp) 171 | } 172 | if rf, ok := ret.Get(0).(func(context.Context, *queryp.QueryParameters) []*gorestapi.Widget); ok { 173 | r0 = rf(ctx, qp) 174 | } else { 175 | if ret.Get(0) != nil { 176 | r0 = ret.Get(0).([]*gorestapi.Widget) 177 | } 178 | } 179 | 180 | if rf, ok := ret.Get(1).(func(context.Context, *queryp.QueryParameters) *int64); ok { 181 | r1 = rf(ctx, qp) 182 | } else { 183 | if ret.Get(1) != nil { 184 | r1 = ret.Get(1).(*int64) 185 | } 186 | } 187 | 188 | if rf, ok := ret.Get(2).(func(context.Context, *queryp.QueryParameters) error); ok { 189 | r2 = rf(ctx, qp) 190 | } else { 191 | r2 = ret.Error(2) 192 | } 193 | 194 | return r0, r1, r2 195 | } 196 | 197 | type mockConstructorTestingTNewGRStore interface { 198 | mock.TestingT 199 | Cleanup(func()) 200 | } 201 | 202 | // NewGRStore creates a new instance of GRStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 203 | func NewGRStore(t mockConstructorTestingTNewGRStore) *GRStore { 204 | mock := &GRStore{} 205 | mock.Mock.Test(t) 206 | 207 | t.Cleanup(func() { mock.AssertExpectations(t) }) 208 | 209 | return mock 210 | } 211 | -------------------------------------------------------------------------------- /mocks/ThingStore.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import context "context" 6 | import gorestapi "github.com/snowzach/gorestapi/gorestapi" 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // ThingStore is an autogenerated mock type for the ThingStore type 10 | type ThingStore struct { 11 | mock.Mock 12 | } 13 | 14 | // ThingDeleteByID provides a mock function with given fields: _a0, _a1 15 | func (_m *ThingStore) ThingDeleteByID(_a0 context.Context, _a1 string) error { 16 | ret := _m.Called(_a0, _a1) 17 | 18 | var r0 error 19 | if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { 20 | r0 = rf(_a0, _a1) 21 | } else { 22 | r0 = ret.Error(0) 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // ThingFind provides a mock function with given fields: _a0 29 | func (_m *ThingStore) ThingFind(_a0 context.Context) ([]*gorestapi.Thing, error) { 30 | ret := _m.Called(_a0) 31 | 32 | var r0 []*gorestapi.Thing 33 | if rf, ok := ret.Get(0).(func(context.Context) []*gorestapi.Thing); ok { 34 | r0 = rf(_a0) 35 | } else { 36 | if ret.Get(0) != nil { 37 | r0 = ret.Get(0).([]*gorestapi.Thing) 38 | } 39 | } 40 | 41 | var r1 error 42 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 43 | r1 = rf(_a0) 44 | } else { 45 | r1 = ret.Error(1) 46 | } 47 | 48 | return r0, r1 49 | } 50 | 51 | // ThingGetByID provides a mock function with given fields: _a0, _a1 52 | func (_m *ThingStore) ThingGetByID(_a0 context.Context, _a1 string) (*gorestapi.Thing, error) { 53 | ret := _m.Called(_a0, _a1) 54 | 55 | var r0 *gorestapi.Thing 56 | if rf, ok := ret.Get(0).(func(context.Context, string) *gorestapi.Thing); ok { 57 | r0 = rf(_a0, _a1) 58 | } else { 59 | if ret.Get(0) != nil { 60 | r0 = ret.Get(0).(*gorestapi.Thing) 61 | } 62 | } 63 | 64 | var r1 error 65 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 66 | r1 = rf(_a0, _a1) 67 | } else { 68 | r1 = ret.Error(1) 69 | } 70 | 71 | return r0, r1 72 | } 73 | 74 | // ThingSave provides a mock function with given fields: _a0, _a1 75 | func (_m *ThingStore) ThingSave(_a0 context.Context, _a1 *gorestapi.Thing) (string, error) { 76 | ret := _m.Called(_a0, _a1) 77 | 78 | var r0 string 79 | if rf, ok := ret.Get(0).(func(context.Context, *gorestapi.Thing) string); ok { 80 | r0 = rf(_a0, _a1) 81 | } else { 82 | r0 = ret.Get(0).(string) 83 | } 84 | 85 | var r1 error 86 | if rf, ok := ret.Get(1).(func(context.Context, *gorestapi.Thing) error); ok { 87 | r1 = rf(_a0, _a1) 88 | } else { 89 | r1 = ret.Error(1) 90 | } 91 | 92 | return r0, r1 93 | } 94 | -------------------------------------------------------------------------------- /mocks/empty.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | // Needed to no have errors during go mod operations if not yet tested 4 | -------------------------------------------------------------------------------- /store/postgres/client.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jmoiron/sqlx" 7 | "github.com/rs/xid" 8 | "github.com/snowzach/golib/store/driver/postgres" 9 | ) 10 | 11 | type Config struct { 12 | postgres.Config `conf:",squash"` 13 | } 14 | 15 | type Client struct { 16 | db *sqlx.DB 17 | newID func() string 18 | } 19 | 20 | // New returns a new database client 21 | func New(cfg *Config) (*Client, error) { 22 | 23 | db, err := postgres.New(&cfg.Config) 24 | if err != nil { 25 | return nil, fmt.Errorf("could not connect to database: %w", err) 26 | } 27 | 28 | return &Client{ 29 | db: db, 30 | newID: func() string { 31 | return xid.New().String() 32 | }, 33 | }, nil 34 | 35 | } 36 | -------------------------------------------------------------------------------- /store/postgres/thing.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | 7 | "github.com/rs/xid" 8 | "github.com/snowzach/golib/store/driver/postgres" 9 | "github.com/snowzach/queryp" 10 | 11 | "github.com/snowzach/gorestapi/gorestapi" 12 | ) 13 | 14 | var ( 15 | ThingTable = postgres.Generate(postgres.Table[gorestapi.Thing]{ 16 | Table: `"thing"`, 17 | Fields: []*postgres.Field[gorestapi.Thing]{ 18 | {Name: "id", ID: true, Insert: "$#", Value: func(rec *gorestapi.Thing) (driver.Value, error) { return rec.ID, nil }}, 19 | {Name: "created", Insert: "NOW()", NullVal: "0001-01-01 00:00:00 UTC"}, 20 | {Name: "updated", Insert: "NOW()", Update: "NOW()", NullVal: "0001-01-01 00:00:00 UTC"}, 21 | {Name: "name", Insert: "$#", Update: "$#", Value: func(rec *gorestapi.Thing) (driver.Value, error) { return rec.Name, nil }}, 22 | {Name: "description", Insert: "$#", Update: "$#", Value: func(rec *gorestapi.Thing) (driver.Value, error) { return rec.Description, nil }}, 23 | }, 24 | Selector: postgres.Selector[gorestapi.Thing]{ 25 | FilterFieldTypes: queryp.FilterFieldTypes{ 26 | "thing.id": queryp.FilterTypeSimple, 27 | "thing.created": queryp.FilterTypeTime, 28 | "thing.updated": queryp.FilterTypeTime, 29 | "thing.name": queryp.FilterTypeString, 30 | "thing.description": queryp.FilterTypeString, 31 | }, 32 | SortFields: queryp.SortFields{ 33 | "thing.id": "", 34 | "thing.created": "", 35 | "thing.updated": "", 36 | "thing.name": "", 37 | "thing.description": "", 38 | }, 39 | DefaultSort: queryp.Sort{ 40 | &queryp.SortTerm{Field: "thing.name", Desc: false}, 41 | }, 42 | }, 43 | }) 44 | ) 45 | 46 | // ThingSave saves the record 47 | func (c *Client) ThingSave(ctx context.Context, record *gorestapi.Thing) error { 48 | if record.ID == "" { 49 | record.ID = xid.New().String() 50 | } 51 | return ThingTable.Upsert(ctx, c.db, record) 52 | } 53 | 54 | // ThingGetByID returns the the record by id 55 | func (c *Client) ThingGetByID(ctx context.Context, id string) (*gorestapi.Thing, error) { 56 | return ThingTable.GetByID(ctx, c.db, id) 57 | } 58 | 59 | // ThingDeleteByID deletes a record by id 60 | func (c *Client) ThingDeleteByID(ctx context.Context, id string) error { 61 | return ThingTable.DeleteByID(ctx, c.db, id) 62 | } 63 | 64 | // ThingsFind fetches records with filter and pagination 65 | func (c *Client) ThingsFind(ctx context.Context, qp *queryp.QueryParameters) ([]*gorestapi.Thing, *int64, error) { 66 | return ThingTable.Selector.Select(ctx, c.db, qp) 67 | } 68 | -------------------------------------------------------------------------------- /store/postgres/widget.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | 7 | "github.com/rs/xid" 8 | "github.com/snowzach/queryp" 9 | 10 | "github.com/snowzach/golib/store/driver/postgres" 11 | "github.com/snowzach/gorestapi/gorestapi" 12 | ) 13 | 14 | var ( 15 | WidgetTable = postgres.Generate(postgres.Table[gorestapi.Widget]{ 16 | Table: `"widget"`, 17 | Fields: []*postgres.Field[gorestapi.Widget]{ 18 | {Name: "id", ID: true, Insert: "$#", Value: func(rec *gorestapi.Widget) (driver.Value, error) { return rec.ID, nil }}, 19 | {Name: "created", Insert: "NOW()", NullVal: "0001-01-01 00:00:00 UTC"}, 20 | {Name: "updated", Insert: "NOW()", Update: "NOW()", NullVal: "0001-01-01 00:00:00 UTC"}, 21 | {Name: "name", Insert: "$#", Update: "$#", Value: func(rec *gorestapi.Widget) (driver.Value, error) { return rec.Name, nil }}, 22 | {Name: "description", Insert: "$#", Update: "$#", Value: func(rec *gorestapi.Widget) (driver.Value, error) { return rec.Description, nil }}, 23 | {Name: "thing_id", Insert: "$#", Update: "$#", Value: func(rec *gorestapi.Widget) (driver.Value, error) { return rec.ThingID, nil }}, 24 | }, 25 | Joins: ` 26 | LEFT JOIN thing ON widget.thing_id = thing.id 27 | `, 28 | Selector: postgres.Selector[gorestapi.Widget]{ 29 | FilterFieldTypes: queryp.FilterFieldTypes{ 30 | "widget.id": queryp.FilterTypeSimple, 31 | "widget.created": queryp.FilterTypeTime, 32 | "widget.updated": queryp.FilterTypeTime, 33 | "widget.name": queryp.FilterTypeString, 34 | "widget.description": queryp.FilterTypeString, 35 | "thing.name": queryp.FilterTypeString, 36 | "thing.description": queryp.FilterTypeString, 37 | }, 38 | SortFields: queryp.SortFields{ 39 | "widget.id": "", 40 | "widget.created": "", 41 | "widget.updated": "", 42 | "widget.name": "", 43 | "widget.description": "", 44 | "thing.name": "", 45 | "thing.description": "", 46 | }, 47 | DefaultSort: queryp.Sort{ 48 | &queryp.SortTerm{Field: "thing.name", Desc: false}, 49 | }, 50 | PostProcessRecord: func(rec *gorestapi.Widget) error { 51 | if rec.ThingID == nil || *rec.ThingID == "" { 52 | rec.Thing = nil 53 | } 54 | return nil 55 | }, 56 | }, 57 | PostProcessRecord: func(rec *gorestapi.Widget) error { 58 | if rec.ThingID == nil || *rec.ThingID == "" { 59 | rec.Thing = nil 60 | } 61 | return nil 62 | }, 63 | SelectAdditionalFields: ThingTable.GenerateAdditionalFields(true), 64 | }) 65 | ) 66 | 67 | // WidgetSave saves the record 68 | func (c *Client) WidgetSave(ctx context.Context, record *gorestapi.Widget) error { 69 | if record.ID == "" { 70 | record.ID = xid.New().String() 71 | } 72 | return WidgetTable.Upsert(ctx, c.db, record) 73 | } 74 | 75 | // WidgetGetByID returns the the record by id 76 | func (c *Client) WidgetGetByID(ctx context.Context, id string) (*gorestapi.Widget, error) { 77 | return WidgetTable.GetByID(ctx, c.db, id) 78 | } 79 | 80 | // WidgetDeleteByID deletes a record by id 81 | func (c *Client) WidgetDeleteByID(ctx context.Context, id string) error { 82 | return WidgetTable.DeleteByID(ctx, c.db, id) 83 | } 84 | 85 | // WidgetsFind fetches records with filter and pagination 86 | func (c *Client) WidgetsFind(ctx context.Context, qp *queryp.QueryParameters) ([]*gorestapi.Widget, *int64, error) { 87 | return WidgetTable.Selector.Select(ctx, c.db, qp) 88 | } 89 | -------------------------------------------------------------------------------- /ui/.env: -------------------------------------------------------------------------------- 1 | VITE_API_URL=/api 2 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:react/jsx-runtime", 6 | "plugin:react-hooks/recommended", 7 | "prettier" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "plugins": ["@typescript-eslint"], 11 | "env": { 12 | "browser": true, 13 | "es2021": true 14 | }, 15 | "settings": { 16 | "react": { 17 | "version": "detect" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # ui 2 | 3 | ## Installation 4 | 5 | Install the application dependencies by running: 6 | 7 | ```sh 8 | yarn 9 | ``` 10 | 11 | ## Development 12 | 13 | Start the application in development mode by running: 14 | 15 | ```sh 16 | yarn dev 17 | ``` 18 | 19 | ## Production 20 | 21 | Build the application in production mode by running: 22 | 23 | ```sh 24 | yarn build 25 | ``` 26 | 27 | ## DataProvider 28 | 29 | The included data provider use [FakeREST](https://github.com/marmelab/fakerest) to simulate a backend. 30 | You'll find a `data.json` file in the `src` directory that includes some fake data for testing purposes. 31 | 32 | It includes two resources, posts and comments. 33 | Posts have the following properties: `id`, `title` and `content`. 34 | Comments have the following properties: `id`, `post_id` and `content`. 35 | 36 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | ui 13 | 109 | 110 | 114 | 115 | 116 | 117 | 118 |
119 |
120 |
Loading...
121 |
122 |
123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "serve": "vite preview", 8 | "type-check": "tsc --noEmit", 9 | "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ./src", 10 | "format": "prettier --write ./src" 11 | }, 12 | "dependencies": { 13 | "lodash": "^4.17.21", 14 | "react": "^18.2.0", 15 | "react-admin": "^4.12.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^18.16.1", 20 | "@types/react": "^18.0.22", 21 | "@types/react-dom": "^18.0.7", 22 | "@typescript-eslint/eslint-plugin": "^5.60.1", 23 | "@typescript-eslint/parser": "^5.60.1", 24 | "@vitejs/plugin-react": "^4.0.1", 25 | "eslint": "^8.43.0", 26 | "eslint-config-prettier": "^8.8.0", 27 | "eslint-plugin-react": "^7.32.2", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "prettier": "^2.8.8", 30 | "typescript": "^5.1.6", 31 | "vite": "^4.3.9" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {} -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowzach/gorestapi/400731a67410b0bd2cee0d685528561ef2a44172/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ui", 3 | "name": "{{name}}", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Admin, 3 | Resource, 4 | } from "react-admin"; 5 | import dataProvider from "./dataProvider"; 6 | 7 | import Dashboard from './resources/Dashboard'; 8 | import { ThingList, ThingEdit, ThingCreate } from './resources/Things'; 9 | import { WidgetList, WidgetEdit, WidgetCreate } from './resources/Widgets'; 10 | 11 | import LocalActivityIcon from '@mui/icons-material/LocalActivity' 12 | import HotTubIcon from '@mui/icons-material/HotTub'; 13 | 14 | export const App = () => ( 15 | 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /ui/src/authProvider.ts.disabled: -------------------------------------------------------------------------------- 1 | import { AuthProvider } from "react-admin"; 2 | import { UserManager } from "oidc-client-ts"; 3 | import decodeJwt from 'jwt-decode'; 4 | 5 | export const authProvider: AuthProvider = { 6 | login: async (config) => { 7 | const userManager = new UserManager({ 8 | authority: config.auth_issuer as string, 9 | client_id: config.auth_client_id as string, 10 | redirect_uri: config.auth_redirect_url as string, 11 | response_type: "code", 12 | scope: config.auth_scopes as string, 13 | }); 14 | 15 | // 1. Redirect to the issuer to ask authentication 16 | await userManager.signinRedirect(); 17 | return; // Do not return anything, the login is still loading 18 | }, 19 | logout: () => { 20 | localStorage.removeItem("token"); 21 | localStorage.removeItem('permissions'); 22 | return Promise.resolve(); 23 | }, 24 | checkError: (error) => { 25 | const status = error.status; 26 | if (status === 401 ) { 27 | // Not authenticated or token expired 28 | localStorage.removeItem('token'); 29 | localStorage.removeItem('permissions'); 30 | return Promise.reject(); 31 | } 32 | // other error code (404, 500, etc): no need to log out 33 | return Promise.resolve(); 34 | }, 35 | checkAuth: () => { 36 | const token = localStorage.getItem("token"); 37 | if (!token) { 38 | return Promise.reject(); 39 | } 40 | // Check the token validity 41 | const jwt = parseToken(token); 42 | const now = new Date(); 43 | return now.getTime() > jwt.exp * 1000 44 | ? Promise.reject() 45 | : Promise.resolve(); 46 | }, 47 | getPermissions: () => { 48 | const persmissions = JSON.parse(localStorage.getItem('permissions') || '[]'); 49 | return persmissions ? Promise.resolve(persmissions) : Promise.reject(); 50 | }, 51 | getIdentity: () => { 52 | const token = window.localStorage.getItem("token"); 53 | const jwt = parseToken(token); 54 | 55 | return Promise.resolve({ 56 | id: jwt.sub, 57 | fullName: jwt.name, 58 | avatar: undefined, 59 | }); 60 | }, 61 | handleCallback: async () => { 62 | 63 | // We came back from the issuer with ?code infos in query params 64 | const { searchParams } = new URL(window.location.href); 65 | const code = searchParams.get("code"); 66 | const state = searchParams.get("state"); 67 | 68 | // oidc-client uses localStorage to keep a temporary state 69 | // between the two redirections. But since we need to send it to the API 70 | // we have to retrieve it manually 71 | const stateKey = `oidc.${state}`; 72 | const { code_verifier } = JSON.parse( 73 | localStorage.getItem(stateKey) || "{}" 74 | ); 75 | 76 | // Transform the code to a token via the API 77 | const response = await fetch(`/api/portal/token`, { 78 | method: "POST", 79 | headers: { "Content-Type": "application/json" }, 80 | body: JSON.stringify({ code: code, code_verifier }), 81 | }); 82 | 83 | if (!response.ok) { 84 | cleanup(); 85 | throw new Error('Failed to handle login callback. You likely do not have access.'); 86 | } 87 | 88 | // Decode the get token response 89 | const token = await response.json(); 90 | if (!token.token_type || token.token_type !== "Bearer" || !token.access_token) { 91 | cleanup(); 92 | throw new Error('Malformed token response.'); 93 | } 94 | 95 | localStorage.setItem("token", token.access_token) 96 | 97 | // Decode the JWT token to get the user permissions 98 | const decodedToken = parseToken(token.access_token); 99 | localStorage.setItem("permissions", JSON.stringify(decodedToken.permissions)); 100 | 101 | // userManager.clearStaleState(); 102 | cleanup(); 103 | return Promise.resolve(); 104 | }, 105 | }; 106 | 107 | export default authProvider; 108 | 109 | export const noAuthProvider = { 110 | // send username and password to the auth server and get back credentials 111 | login: () => Promise.resolve(), 112 | // when the dataProvider returns an error, check if this is an authentication error 113 | checkError: () => Promise.resolve(), 114 | // when the user navigates, make sure that their credentials are still valid 115 | checkAuth: () => Promise.resolve(), 116 | // remove local credentials and notify the auth server that the user logged out 117 | logout: () => Promise.resolve(), 118 | // get the user's profile 119 | getIdentity: () => Promise.resolve({ 120 | id: 'unknown', 121 | fullName: 'unknown', 122 | avatar: undefined, 123 | }), 124 | // get the user permissions (optional) 125 | getPermissions: () => Promise.resolve(['reserve_admin','user_admin','finops']), 126 | }; 127 | 128 | const cleanup = () => { 129 | // Remove the ?code&state from the URL 130 | window.history.replaceState( 131 | {}, 132 | window.document.title, 133 | window.location.origin 134 | ); 135 | }; 136 | 137 | type JWT = { 138 | sub: string; 139 | exp: number; 140 | name: string; 141 | permissions: string[]; 142 | } 143 | 144 | const parseToken = (tokenJson: string | null): JWT => { 145 | const unknownUser = { 146 | sub: 'unknown', 147 | exp: 0, 148 | name: 'unknown', 149 | permissions: [], 150 | } 151 | if (tokenJson == null) return unknownUser; 152 | const token = decodeJwt(tokenJson) as JWT; 153 | return token || unknownUser; 154 | } -------------------------------------------------------------------------------- /ui/src/dataOverrides.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | type SearchOverrides = { [index: string]: Search }; 4 | type SortOverrides = { [index: string]: string }; 5 | 6 | type Search = { 7 | operator?: string 8 | }; 9 | 10 | type Override = { 11 | path?: string 12 | idField?: string 13 | search?: SearchOverrides 14 | sort?: SortOverrides 15 | }; 16 | 17 | type Overrides = { [index: string]: Override }; 18 | 19 | const overrides: Overrides = { 20 | whateverResource: { 21 | path: 'whatever/path', 22 | idField: 'whatever_id', 23 | search: { 24 | 'name': { operator: "=~~" }, 25 | 'whatever_field': { operator: "=~~" }, 26 | } 27 | }, 28 | portalConfig: { 29 | path: 'portal/config', 30 | }, 31 | strategy: { 32 | idField: 'source', 33 | }, 34 | quotes: { 35 | path: 'quotes', 36 | search: { 37 | 'list': { operator: "=~~" }, 38 | 'address': { operator: "=~~" }, 39 | 'comment': { operator: "=~~" }, 40 | }, 41 | sort: { 42 | 'createdAt.value': '-createdAt.value' 43 | } 44 | }, 45 | rfqConfig: { 46 | path: 'rfq/config', 47 | search: { 48 | 'id': { operator: "=~~" }, 49 | } 50 | }, 51 | rfqConfigSafeties: { 52 | path: 'rfq/config/safeties', 53 | search: { 54 | 'id': { operator: "=~~" }, 55 | } 56 | }, 57 | rfqConfigFees: { 58 | path: 'rfq/config/fees', 59 | search: { 60 | 'id': { operator: "=~~" }, 61 | } 62 | }, 63 | assetClasses: { 64 | path: 'assets/class', 65 | search: { 66 | 'name': { operator: "=~~" }, 67 | 'description': { operator: "=~~" }, 68 | } 69 | }, 70 | portalUsers: { 71 | path: 'portal/users', 72 | search: { 73 | 'name': { operator: "=~~" }, 74 | 'description': { operator: "=~~" }, 75 | } 76 | }, 77 | hrAssets: { 78 | path: 'hiddenroad/assets', 79 | idField: 'symbol', 80 | }, 81 | hrCounterparties: { 82 | path: 'hiddenroad/counterparties', 83 | idField: 'counterparty', 84 | }, 85 | hrAccountBalances: { 86 | path: 'hiddenroad/accounts/balances', 87 | idField: 'account.id', 88 | }, 89 | hrInstruments: { 90 | path: 'hiddenroad/instruments', 91 | idField: 'symbol', 92 | }, 93 | hrMarkets: { 94 | path: 'hiddenroad/markets', 95 | idField: 'symbol', 96 | }, 97 | hrTrades: { 98 | path: 'hiddenroad/trades', 99 | idField: 'trade_id', 100 | }, 101 | } 102 | 103 | export default overrides; -------------------------------------------------------------------------------- /ui/src/dataProvider.ts: -------------------------------------------------------------------------------- 1 | import { DataProvider } from 'ra-core'; 2 | import { each, isObject, isArray } from 'lodash'; 3 | import dataOverrides from './dataOverrides'; 4 | 5 | import { fetchUtils, HttpError } from 'react-admin'; 6 | 7 | export const apiUrl = import.meta.env.VITE_API_URL; 8 | const httpClient = fetchUtils.fetchJson; 9 | 10 | // If we want to also support authentication 11 | // const httpClient = (url: string, options = {}) => { 12 | // const token = localStorage.getItem('token'); 13 | // let user = {}; 14 | // if (token) { 15 | // user = { token: `Bearer ${token}`, authenticated: !!token }; 16 | // } 17 | // return fetchUtils.fetchJson(url, {...options, user}); 18 | // } 19 | 20 | const api: DataProvider = { 21 | 22 | // getList returns a list of resources 23 | getList: (resource, params) => { 24 | 25 | // Build the filter 26 | let query = buildFilter(resource, params); 27 | 28 | // Build Sort 29 | if(params.sort) { 30 | const { field, order } = params.sort; 31 | const sortName = getObjectField(dataOverrides, resource, "sort", field) 32 | if(sortName) { 33 | query += `&sort=${order === 'DESC' ? '-' : ''}${sortName}` 34 | } else { 35 | query += `&sort=${order === 'DESC' ? '-' : ''}${field}` 36 | } 37 | 38 | } 39 | 40 | // Build Pagination 41 | if(params.pagination) { 42 | const { page, perPage } = params.pagination; 43 | query += `&offset=${(page-1)*perPage}&limit=${perPage}`; 44 | } 45 | 46 | // Build URL 47 | const url = `${apiUrl}/${pathByResource(resource)}?`+query.substring(1); 48 | 49 | return httpClient(url).then(({ headers, json }) => { 50 | // If the id field in the record is overridden it must be transformed. 51 | const idField = getObjectField(dataOverrides, resource, "idField") 52 | if (idField) { 53 | json.results.forEach((row: any, index: number) => { 54 | row.id = row[idField]; 55 | json.results[index] = row; 56 | }); 57 | } 58 | 59 | // If pagination is enabled, there are results and no/zero count 60 | // we need to provide the pageInfo object 61 | if(params.pagination && json.results.length && !json.count) { 62 | return { 63 | data: json.results, 64 | pageInfo: { 65 | hasPreviousPage: (params.pagination.page > 1), 66 | hasNextPage: json.results.length == params.pagination.perPage, 67 | } 68 | }; 69 | } 70 | 71 | return { 72 | data: json.results, 73 | total: json.count, 74 | } 75 | }); 76 | }, 77 | 78 | // getOne gets one record by id 79 | getOne: (resource, params) => { 80 | return httpClient(`${apiUrl}/${pathByResource(resource)}/${params.id}`).then(({ json }) => { 81 | // If the id field in the record is overridden it must be transformed. 82 | const idField = getObjectField(dataOverrides, resource, "idField") 83 | if (idField) { 84 | json.id = json[idField]; 85 | } 86 | return { 87 | data: json, 88 | } 89 | }) 90 | 91 | }, 92 | 93 | // getMany fetches many records by IDs 94 | getMany: (resource, params) => { 95 | const query = `id=(${params.ids.join(',')})`; 96 | const url = `${apiUrl}/${pathByResource(resource)}?${query}`; 97 | 98 | return httpClient(url).then(({ json }) => { 99 | // If the id field in the record is overridden it must be transformed. 100 | const idField = getObjectField(dataOverrides, resource, "idField") 101 | if (idField) { 102 | json.results.forEach((row: any, index: number) => { 103 | row.id = row[idField]; 104 | json.results[index] = row; 105 | }); 106 | } 107 | 108 | return ({ 109 | data: json.results 110 | }); 111 | }); 112 | }, 113 | 114 | // getManyReference returns a list of resource related to another resource (all comments for a post) 115 | getManyReference: (resource: string, params: any) => { 116 | 117 | let query = ''; 118 | let filter = flattenObject(params.filter); 119 | 120 | // Build Filter 121 | for(let field in filter) { 122 | query += `&${field}=` 123 | let value = filter[field]; 124 | if(typeof value == 'object') { 125 | query += `(${value.join(',')})`; 126 | } else { 127 | query += `"${value}"`; 128 | } 129 | } 130 | 131 | query += `&${params.target}=` 132 | if(typeof params.id == 'object') { 133 | query += `(${params.id.join(',')})`; 134 | } else { 135 | query += `"${params.id}"`; 136 | } 137 | 138 | // Build Sort 139 | const { field, order } = params.sort; 140 | if(order) { 141 | query += `&sort=${order === 'DESC' ? '-' : ''}${field}` 142 | } 143 | 144 | // Build Pagination 145 | const { page, perPage } = params.pagination; 146 | query += `&offset=${(page-1)*perPage}&limit=${perPage}`; 147 | 148 | // Build URL 149 | const url = `${apiUrl}/${pathByResource(resource)}?`+query.substring(1); 150 | 151 | return httpClient(url).then(({ headers, json }) => ({ 152 | data: json.results, 153 | total: json.count, 154 | })); 155 | }, 156 | 157 | // update updates one resource 158 | update: (resource, params) => 159 | httpClient(`${apiUrl}/${pathByResource(resource)}`, { 160 | method: 'POST', 161 | body: JSON.stringify(params.data), 162 | }).then(({ json }) => { 163 | // If the id field in the record is overridden it must be transformed. 164 | const idField = getObjectField(dataOverrides, resource, "idField") 165 | if (idField) { 166 | json.id = json[idField]; 167 | } 168 | return { 169 | data: json, 170 | } 171 | }).catch((err) => { 172 | if(err.body.error) { 173 | return Promise.reject(new HttpError(err.body.error, err.status)); 174 | } 175 | return Promise.reject(err); 176 | }), 177 | 178 | // updateMany updates many resources 179 | updateMany: (resource, params) => { 180 | console.log("Not supported..."); 181 | return Promise.resolve({data:[]}); 182 | }, 183 | 184 | // create creates on resource 185 | create: (resource, params) => 186 | httpClient(`${apiUrl}/${pathByResource(resource)}`, { 187 | method: 'POST', 188 | body: JSON.stringify(params.data), 189 | }).then(({ json }) => { 190 | // If the id field in the record is overridden it must be transformed. 191 | const idField = getObjectField(dataOverrides, resource, "idField") 192 | if (idField) { 193 | json.id = json[idField]; 194 | } 195 | return { 196 | data: json, 197 | } 198 | }).catch((err) => { 199 | if(err.body.error) { 200 | return Promise.reject(new HttpError(err.body.error, err.status)); 201 | } 202 | return Promise.reject(err); 203 | }), 204 | 205 | // delete a resource 206 | delete: (resource, params) => 207 | httpClient(`${apiUrl}/${pathByResource(resource)}/${params.id}`, { 208 | method: 'DELETE', 209 | }).then(({json}) => ({ 210 | data: { id: params.id, ...json }, 211 | })).catch((err) => { 212 | if(err.body.error) { 213 | return Promise.reject(new HttpError(err.body.error, err.status)); 214 | } 215 | return Promise.reject(err); 216 | }), 217 | 218 | // delete many resources 219 | deleteMany: (resource, params) => { 220 | // const query = { 221 | // filter: JSON.stringify({ id: params.ids}), 222 | // }; 223 | // return httpClient(`${apiUrl}/${pathByResource(resource)}?${stringify(query)}`, { 224 | // method: 'DELETE', 225 | // }).then(({ json }) => ({ data: json })); 226 | console.log("Not supported..."); 227 | return Promise.resolve({data:[]}); 228 | }, 229 | 230 | // get fetches anything from the API 231 | get: (path: string) => { 232 | const url = `${apiUrl}/${path}`; 233 | return httpClient(url).then(({ json }) => (json));; 234 | }, 235 | 236 | }; 237 | 238 | export default api; 239 | 240 | // getObjectField traverses an object by field names in args and returns the field 241 | const getObjectField = (obj: any, ...args: string[]): any => { 242 | return args.reduce((obj, level) => obj && obj[level], obj) 243 | } 244 | 245 | // pathByResource gets the path for a resource taking into consideration and overrides 246 | export const pathByResource = (resource: string): string => { 247 | if (resource in dataOverrides) { 248 | let override = dataOverrides[resource]; 249 | if (override && override.path) { 250 | return override.path; 251 | } 252 | } 253 | return resource; 254 | } 255 | 256 | // flattenObject turns an object into a single level object with keys of period delimited values. 257 | export const flattenObject = (obj: any): {[key: string]: any} => { 258 | var nobj: {[key: string]: any} = {}; 259 | each(obj, function(val: any, key: any){ 260 | // ensure is JSON key-value map, not array 261 | if (isObject(val) && !isArray(val)) { 262 | // union the returned result by concat all keys 263 | var strip = flattenObject(val) 264 | each(strip, function(v,k){ 265 | nobj[key+'.'+k] = v 266 | }) 267 | } else { 268 | nobj[key] = val 269 | } 270 | }) 271 | return nobj 272 | } 273 | 274 | // buildFilter will build the queryp compatible filter from the search options 275 | // taking into account any overrides. 276 | const buildFilter = (resource: string, params: any):string => { 277 | 278 | const searchOverrides = getObjectField(dataOverrides, resource, "search") 279 | 280 | // If search is a function, call it to get the filter 281 | if (searchOverrides instanceof Function) { 282 | return searchOverrides(params); 283 | } 284 | 285 | let ret = ''; 286 | 287 | // Handle the various forms of filter.option 288 | if(params.filter && params.filter.option) { 289 | const options = params.filter.option 290 | if(options instanceof Object ) { 291 | if(options instanceof Array) { 292 | options.forEach((option) => { 293 | ret += `&option=${option}`; 294 | }); 295 | } else { 296 | for(let option in options){ 297 | ret += `&option[${option}]=${options[option]}`; 298 | } 299 | } 300 | } else { 301 | ret += `&option=${options}` 302 | } 303 | delete params.filter.option; 304 | } 305 | 306 | const filter = flattenObject(params.filter); 307 | 308 | // If search values have been overridden 309 | if (searchOverrides) { 310 | for(let field in filter) { 311 | const search = searchOverrides[field]; 312 | const value = filter[field]; 313 | if(value === undefined) continue 314 | if (search) { 315 | if (search.field) { 316 | ret += `&${encodeURIComponent(search.field)}` 317 | } else { 318 | ret += `&${encodeURIComponent(field)}` 319 | } 320 | if (search.operator) { 321 | ret += search.operator 322 | } else { 323 | ret += '=' 324 | } 325 | } else { 326 | ret += `&${field}=` 327 | } 328 | if(typeof value == 'object') { 329 | ret += `("${encodeURIComponent(value.join('","'))}")`; 330 | } else if(typeof value == 'number' || typeof value == 'boolean') { 331 | ret += value.toString(); 332 | } else { 333 | if(value.charAt(0) === '(' && value.charAt(value.length-1) === ')') { 334 | ret += `${encodeURIComponent(value)}`; 335 | } else { 336 | ret += `"${encodeURIComponent(value)}"`; 337 | } 338 | } 339 | } 340 | } else { 341 | for(let field in filter) { 342 | const value = filter[field]; 343 | if(value === undefined) continue 344 | ret += `&${field}=` 345 | if(typeof value == 'object') { 346 | ret += `(${encodeURIComponent(value.join(','))})`; 347 | } else if(typeof value == 'number') { 348 | ret += value.toString(); 349 | } else { 350 | if(value.charAt(0) === '(' && value.charAt(value.length-1) === ')') { 351 | ret += `${encodeURIComponent(value)}`; 352 | } else { 353 | ret += `"${encodeURIComponent(value)}"`; 354 | } 355 | } 356 | } 357 | } 358 | 359 | return ret; 360 | } 361 | -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { App } from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /ui/src/resources/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader } from '@mui/material'; 2 | import { Title } from 'react-admin'; 3 | 4 | const dashboard = () => ( 5 | 6 | 7 | <CardHeader title="Welcome to the gorestapi dashboard" /> 8 | <CardContent>Select from the options on the left.</CardContent> 9 | </Card> 10 | ); 11 | 12 | export default dashboard; -------------------------------------------------------------------------------- /ui/src/resources/Things.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | List, 3 | Edit, 4 | Create, 5 | Filter, 6 | SimpleForm, 7 | Datagrid, 8 | TextField, 9 | DateField, 10 | EditButton, 11 | TextInput, 12 | ReferenceInput, 13 | SelectInput, 14 | Pagination 15 | } from 'react-admin'; 16 | 17 | const ListPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100]} />; 18 | 19 | const listFilters = [ 20 | <TextInput label="Search" source="q" alwaysOn />, 21 | <ReferenceInput label="Name" source="name" reference="things" allowEmpty> 22 | <SelectInput source="name" optionValue="name" optionText="name" /> 23 | </ReferenceInput> 24 | ]; 25 | 26 | export const ThingList = () => ( 27 | <List sort={{ field: 'id', order: 'DESC'}} pagination={<ListPagination />} filters={listFilters} > 28 | <Datagrid> 29 | <TextField source="name" /> 30 | <TextField source="description" /> 31 | <DateField source="created" showTime={true} /> 32 | <DateField source="updated" showTime={true} /> 33 | <EditButton /> 34 | </Datagrid> 35 | </List> 36 | ); 37 | 38 | export const ThingEdit = () => ( 39 | <Edit> 40 | <SimpleForm> 41 | <TextInput source="id" /> 42 | <TextInput source="name" /> 43 | <TextInput multiline source="description" /> 44 | </SimpleForm> 45 | </Edit> 46 | ); 47 | 48 | export const ThingCreate = () => ( 49 | <Create> 50 | <SimpleForm> 51 | <TextInput source="name" /> 52 | <TextInput multiline source="description" /> 53 | </SimpleForm> 54 | </Create> 55 | ); 56 | -------------------------------------------------------------------------------- /ui/src/resources/Widgets.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | List, 3 | Datagrid, 4 | TextField, 5 | ReferenceField, 6 | Filter, 7 | DateField, 8 | EditButton, 9 | Edit, 10 | Create, 11 | SimpleForm, 12 | ReferenceInput, 13 | SelectInput, 14 | TextInput 15 | } from 'react-admin'; 16 | 17 | const WidgetFilter = () => ( 18 | <Filter> 19 | <TextInput label="Search" source="name" alwaysOn /> 20 | <ReferenceInput label="Thing" source="thing_id" reference="things" allowEmpty> 21 | <SelectInput optionText="name" /> 22 | </ReferenceInput> 23 | </Filter> 24 | ); 25 | 26 | export const WidgetList = () => ( 27 | <List filters={<WidgetFilter />}> 28 | <Datagrid rowClick="edit"> 29 | <TextField source="name" /> 30 | <TextField source="description" /> 31 | <ReferenceField source="thing_id" reference="things"> 32 | <TextField source="name" /> 33 | </ReferenceField> 34 | <DateField source="created" showTime={true} /> 35 | <DateField source="updated" showTime={true} /> 36 | <EditButton /> 37 | </Datagrid> 38 | </List> 39 | ); 40 | 41 | export const WidgetEdit = () => ( 42 | <Edit> 43 | <SimpleForm> 44 | <TextInput source="name" /> 45 | <TextInput multiline source="description" /> 46 | <ReferenceInput source="thing_id" reference="things"> 47 | <SelectInput optionText="name" /> 48 | </ReferenceInput> 49 | </SimpleForm> 50 | </Edit> 51 | ); 52 | 53 | 54 | export const WidgetCreate = () => ( 55 | <Create> 56 | <SimpleForm> 57 | <TextInput source="name" /> 58 | <TextInput multiline source="description" /> 59 | <ReferenceInput source="thing_id" reference="things"> 60 | <SelectInput optionText="name" /> 61 | </ReferenceInput> 62 | </SimpleForm> 63 | </Create> 64 | ); -------------------------------------------------------------------------------- /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | host: '0.0.0.0', 9 | proxy: { 10 | '/api': { 11 | target: 'http://localhost:8080', 12 | changeOrigin: true, 13 | // rewrite: (path) => path.replace(/^\/api/, ''), 14 | } 15 | }, 16 | } 17 | }) 18 | --------------------------------------------------------------------------------