├── .dockerignore ├── .gitignore ├── .idea ├── .gitignore ├── food-trucks.iml ├── jsLibraryMappings.xml ├── modules.xml └── vcs.xml ├── Dockerfile ├── backend ├── cmds │ ├── cli │ │ └── main.go │ └── web │ │ └── main.go ├── configs │ ├── cli.yaml │ ├── data.csv │ └── web.yaml ├── go.mod ├── go.sum ├── packages │ ├── controllers │ │ └── facilityCtl.go │ ├── models │ │ └── facility.go │ ├── services │ │ ├── facilitySvc.go │ │ ├── facilitySvc_test.go │ │ └── interfaces.go │ └── util │ │ ├── errs │ │ └── errs.go │ │ ├── irisbase │ │ └── irisbase.go │ │ ├── rdb │ │ ├── basic.go │ │ ├── basic_test.go │ │ ├── client.go │ │ ├── client_test.go │ │ ├── di_test.go │ │ ├── entityStore.go │ │ ├── entityStore_test.go │ │ ├── geoStore.go │ │ ├── interfaces.go │ │ ├── sliceStore.go │ │ ├── types.go │ │ └── util.go │ │ ├── safeslice │ │ └── safeslice.go │ │ ├── singleflight │ │ ├── singleflight.go │ │ └── singleflight_test.go │ │ └── yaml │ │ └── utils.go └── web │ ├── assets │ ├── Inter-italic.var-d1401419.woff2 │ ├── Inter-roman.var-17fe38ab.woff2 │ ├── index-67517a09.js │ ├── index-8b962636.css │ ├── primeicons-131bc3bf.ttf │ ├── primeicons-3824be50.woff2 │ ├── primeicons-5e10f102.svg │ ├── primeicons-90a58d3a.woff │ └── primeicons-ce852338.eot │ ├── index.html │ └── vite.svg ├── doc └── images │ ├── cli.png │ ├── hexagonal.png │ ├── home-page.png │ ├── pop.png │ └── your-location.png ├── docker-compose.yml ├── frontend ├── .env.development ├── assets │ └── react.svg ├── dist │ ├── assets │ │ ├── Inter-italic.var-d1401419.woff2 │ │ ├── Inter-roman.var-17fe38ab.woff2 │ │ ├── index-67517a09.js │ │ ├── index-8b962636.css │ │ ├── primeicons-131bc3bf.ttf │ │ ├── primeicons-3824be50.woff2 │ │ ├── primeicons-5e10f102.svg │ │ ├── primeicons-90a58d3a.woff │ │ └── primeicons-ce852338.eot │ ├── index.html │ └── vite.svg ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── config.ts │ ├── index.css │ ├── main.tsx │ ├── map │ │ ├── FacilityMarkers.tsx │ │ ├── Map.tsx │ │ └── SwitchLocation.tsx │ ├── models │ │ └── facility.ts │ ├── utils │ │ ├── fetcher.ts │ │ └── getDistance.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── readme.md └── scripts ├── build_front.sh └── start_redis.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | frentend/node_modules 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | frontend/node_modules -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/food-trucks.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a Node.js base image 2 | FROM node:latest as builder 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json 8 | COPY frontend/package.json . 9 | COPY frontend/pnpm-lock.yaml . 10 | 11 | # Install dependencies using PNPM 12 | RUN npm install -g pnpm 13 | RUN pnpm install 14 | 15 | # Copy the rest of the application code 16 | COPY frontend . 17 | 18 | # Build the React app 19 | RUN pnpm run build 20 | 21 | 22 | # Use the official Go image 23 | FROM golang:latest 24 | 25 | # Set working directory 26 | WORKDIR /go/src/app 27 | 28 | 29 | # Copy the local package files to the container's workspace 30 | COPY backend . 31 | 32 | # Copy frontend to web 33 | COPY --from=builder /app/dist ./web 34 | 35 | # Install Iris 36 | RUN go get -u github.com/kataras/iris/v12 37 | 38 | # Build the Go application 39 | RUN go build -o main cmds/web/main.go 40 | RUN go build -o food-cli cmds/cli/main.go 41 | # Expose port 8080 to the outside world 42 | EXPOSE 8080 43 | 44 | # Command to run the executable 45 | CMD ["./main"] 46 | -------------------------------------------------------------------------------- /backend/cmds/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "food-trucks/packages/models" 8 | "food-trucks/packages/services" 9 | "food-trucks/packages/util/rdb" 10 | "food-trucks/packages/util/yaml" 11 | "os" 12 | ) 13 | 14 | type CliConfig struct { 15 | Redis rdb.Config `yaml:"redis"` 16 | } 17 | 18 | func main() { 19 | svc := mustInit() 20 | reader := bufio.NewReader(os.Stdin) 21 | ctx := context.Background() 22 | 23 | for { 24 | fmt.Print("Enter Food Item to search facility: ") 25 | item, err := reader.ReadString('\n') 26 | if err != nil { 27 | panic(err) 28 | } 29 | facilities, err := svc.GetByItem(ctx, item) 30 | if err != nil { 31 | panic(err) 32 | } 33 | for _, facility := range facilities { 34 | fmt.Println(facility.Applicant + " " + facility.LocationDescription) 35 | } 36 | } 37 | } 38 | 39 | func mustInit() *services.FacilitySvc { 40 | config, err := yaml.ParseYaml[CliConfig]("./configs/cli.yaml") 41 | if err != nil { 42 | panic(err) 43 | } 44 | facilityStore := rdb.NewEntityStore[string, models.Facility]("facility", 0, config.Redis). 45 | WithGetKey(models.GetFacilityKey) 46 | itemFacilityStore := rdb.NewSliceStore[string, models.Facility]("item", 0, config.Redis, facilityStore). 47 | WithGetKey(models.GetFacilityKey).WithGetScore(models.GetFacilityScore) 48 | geoFacilityStore := rdb.NewGeoStore[string, models.Facility]("geo", 0, config.Redis, facilityStore). 49 | WithGetKey(models.GetFacilityKey).WithGetLocation(models.GetFacilityLocation) 50 | facilitySvc := &services.FacilitySvc{ 51 | FacilityStore: facilityStore, 52 | ItemFacilityStore: itemFacilityStore, 53 | GeoFacilityStore: geoFacilityStore, 54 | } 55 | err = facilitySvc.Seed("./configs/data.csv") 56 | if err != nil { 57 | panic(err) 58 | } 59 | return facilitySvc 60 | } 61 | -------------------------------------------------------------------------------- /backend/cmds/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "food-trucks/packages/controllers" 6 | "food-trucks/packages/models" 7 | "food-trucks/packages/services" 8 | "food-trucks/packages/util/irisbase" 9 | "food-trucks/packages/util/rdb" 10 | "food-trucks/packages/util/yaml" 11 | ) 12 | 13 | type WebConfig struct { 14 | irisbase.AppConfig `yaml:"appConfig"` 15 | Redis rdb.Config `yaml:"redis"` 16 | } 17 | 18 | type App struct { 19 | *irisbase.App 20 | WebConfig 21 | } 22 | 23 | type AppBuilder struct { 24 | WebConfig 25 | } 26 | 27 | func (b AppBuilder) BadRequest() []error { 28 | return []error{} 29 | } 30 | 31 | func (b AppBuilder) Services() []any { 32 | redisConfig := b.WebConfig.Redis 33 | fmt.Println("redisConfig:", redisConfig) 34 | facilityStore := rdb.NewEntityStore[string, models.Facility]("facility", 0, redisConfig). 35 | WithGetKey(models.GetFacilityKey) 36 | itemFacilityStore := rdb.NewSliceStore[string, models.Facility]("item", 0, redisConfig, facilityStore). 37 | WithGetKey(models.GetFacilityKey).WithGetScore(models.GetFacilityScore) 38 | geoFacilityStore := rdb.NewGeoStore[string, models.Facility]("geo", 0, redisConfig, facilityStore). 39 | WithGetKey(models.GetFacilityKey).WithGetLocation(models.GetFacilityLocation) 40 | facilitySvc := &services.FacilitySvc{ 41 | FacilityStore: facilityStore, 42 | ItemFacilityStore: itemFacilityStore, 43 | GeoFacilityStore: geoFacilityStore, 44 | } 45 | err := facilitySvc.Seed("./configs/data.csv") 46 | if err != nil { 47 | panic(err) 48 | } 49 | return []any{facilitySvc} 50 | } 51 | 52 | func (b AppBuilder) Controller() map[string]any { 53 | return map[string]any{ 54 | "/facilities/": new(controllers.FacilityCtl), 55 | } 56 | } 57 | 58 | func main() { 59 | config, err := yaml.ParseYaml[WebConfig]("./configs/web.yaml") 60 | if err != nil { 61 | panic(err) 62 | } 63 | app := &App{ 64 | WebConfig: *config, 65 | } 66 | app.App = irisbase.NewIrisApp(config.AppConfig, AppBuilder{WebConfig: *config}) 67 | app.Start() 68 | } 69 | -------------------------------------------------------------------------------- /backend/configs/cli.yaml: -------------------------------------------------------------------------------- 1 | redis: 2 | enabled : true 3 | addr: redis:6379 4 | prefix: food -------------------------------------------------------------------------------- /backend/configs/web.yaml: -------------------------------------------------------------------------------- 1 | appConfig: 2 | apiPrefix: /api 3 | port: 8080 4 | debug: true 5 | redis: 6 | enabled : true 7 | addr: redis:6379 8 | prefix: food -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module food-trucks 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.3.2 // indirect 7 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect 8 | github.com/CloudyKit/jet/v6 v6.2.0 // indirect 9 | github.com/Joker/jade v1.1.3 // indirect 10 | github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect 11 | github.com/andybalholm/brotli v1.1.0 // indirect 12 | github.com/aymerick/douceur v0.2.0 // indirect 13 | github.com/blang/semver/v4 v4.0.0 // indirect 14 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | github.com/fatih/structs v1.1.0 // indirect 17 | github.com/flosch/pongo2/v4 v4.0.2 // indirect 18 | github.com/gobwas/httphead v0.1.0 // indirect 19 | github.com/gobwas/pool v0.2.1 // indirect 20 | github.com/gobwas/ws v1.3.2 // indirect 21 | github.com/golang/snappy v0.0.4 // indirect 22 | github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect 23 | github.com/google/uuid v1.6.0 // indirect 24 | github.com/gorilla/css v1.0.0 // indirect 25 | github.com/gorilla/websocket v1.5.1 // indirect 26 | github.com/iris-contrib/schema v0.0.6 // indirect 27 | github.com/josharian/intern v1.0.0 // indirect 28 | github.com/kataras/blocks v0.0.8 // indirect 29 | github.com/kataras/golog v0.1.11 // indirect 30 | github.com/kataras/iris/v12 v12.2.11 // indirect 31 | github.com/kataras/neffos v0.0.24-0.20240408172741-99c879ba0ede // indirect 32 | github.com/kataras/pio v0.0.13 // indirect 33 | github.com/kataras/sitemap v0.0.6 // indirect 34 | github.com/kataras/tunnel v0.0.4 // indirect 35 | github.com/klauspost/compress v1.17.7 // indirect 36 | github.com/mailgun/raymond/v2 v2.0.48 // indirect 37 | github.com/mailru/easyjson v0.7.7 // indirect 38 | github.com/mediocregopher/radix/v3 v3.8.1 // indirect 39 | github.com/microcosm-cc/bluemonday v1.0.26 // indirect 40 | github.com/nats-io/nats.go v1.34.1 // indirect 41 | github.com/nats-io/nkeys v0.4.7 // indirect 42 | github.com/nats-io/nuid v1.0.1 // indirect 43 | github.com/redis/go-redis/v9 v9.5.1 // indirect 44 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 45 | github.com/samber/lo v1.39.0 // indirect 46 | github.com/schollz/closestmatch v2.1.0+incompatible // indirect 47 | github.com/sirupsen/logrus v1.8.1 // indirect 48 | github.com/tdewolff/minify/v2 v2.20.19 // indirect 49 | github.com/tdewolff/parse/v2 v2.7.12 // indirect 50 | github.com/valyala/bytebufferpool v1.0.0 // indirect 51 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 52 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 53 | github.com/yosssi/ace v0.0.5 // indirect 54 | golang.org/x/crypto v0.22.0 // indirect 55 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect 56 | golang.org/x/net v0.24.0 // indirect 57 | golang.org/x/sync v0.7.0 // indirect 58 | golang.org/x/sys v0.19.0 // indirect 59 | golang.org/x/text v0.14.0 // indirect 60 | golang.org/x/time v0.5.0 // indirect 61 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 62 | google.golang.org/protobuf v1.33.0 // indirect 63 | gopkg.in/ini.v1 v1.67.0 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= 4 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= 5 | github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= 6 | github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= 7 | github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= 8 | github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= 9 | github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= 10 | github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= 11 | github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= 12 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= 13 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 14 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 15 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 16 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 17 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 18 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 19 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 20 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 21 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 26 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 27 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 28 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 29 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 30 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 31 | github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= 32 | github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= 33 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 34 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 35 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 36 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 37 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 38 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 39 | github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= 40 | github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= 41 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 42 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 43 | github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU= 44 | github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= 45 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 46 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 47 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 48 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 49 | github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= 50 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= 51 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 52 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 53 | github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= 54 | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 55 | github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= 56 | github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= 57 | github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= 58 | github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= 59 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 60 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 61 | github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= 62 | github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= 63 | github.com/kataras/golog v0.1.11 h1:dGkcCVsIpqiAMWTlebn/ZULHxFvfG4K43LF1cNWSh20= 64 | github.com/kataras/golog v0.1.11/go.mod h1:mAkt1vbPowFUuUGvexyQ5NFW6djEgGyxQBIARJ0AH4A= 65 | github.com/kataras/iris/v12 v12.2.11 h1:sGgo43rMPfzDft8rjVhPs6L3qDJy3TbBrMD/zGL1pzk= 66 | github.com/kataras/iris/v12 v12.2.11/go.mod h1:uMAeX8OqG9vqdhyrIPv8Lajo/wXTtAF43wchP9WHt2w= 67 | github.com/kataras/neffos v0.0.24-0.20240408172741-99c879ba0ede h1:ZnSJQ+ri9x46Yz15wHqSb93Q03yY12XMVLUFDJJ0+/g= 68 | github.com/kataras/neffos v0.0.24-0.20240408172741-99c879ba0ede/go.mod h1:i0dtcTbpnw1lqIbojYtGtZlu6gDWPxJ4Xl2eJ6oQ1bE= 69 | github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= 70 | github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= 71 | github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= 72 | github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= 73 | github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= 74 | github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= 75 | github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= 76 | github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 77 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 78 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 79 | github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= 80 | github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= 81 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 82 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 83 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 84 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 85 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 86 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 87 | github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= 88 | github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= 89 | github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= 90 | github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= 91 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 92 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 93 | github.com/nats-io/nats.go v1.34.1 h1:syWey5xaNHZgicYBemv0nohUPPmaLteiBEUT6Q5+F/4= 94 | github.com/nats-io/nats.go v1.34.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= 95 | github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= 96 | github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= 97 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= 98 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 99 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 100 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 101 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 102 | github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= 103 | github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= 104 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 105 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 106 | github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= 107 | github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 108 | github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= 109 | github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= 110 | github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= 111 | github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= 112 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 113 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 114 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 115 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 116 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 117 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 118 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 119 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 120 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 121 | github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo= 122 | github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM= 123 | github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ= 124 | github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= 125 | github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= 126 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 127 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 128 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 129 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 130 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 131 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 132 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 133 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 134 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 135 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 136 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 137 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 138 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= 139 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 140 | github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= 141 | github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= 142 | github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= 143 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 144 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= 145 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 146 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 147 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 148 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 149 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 150 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 151 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 152 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 153 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= 154 | golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 155 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 156 | golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 157 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 158 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 159 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 160 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 161 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 162 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 165 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 166 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 167 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 172 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 173 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 174 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 175 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 176 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 177 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 178 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 179 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 180 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 181 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 182 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 183 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 184 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 185 | golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 186 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 187 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 188 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 189 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 190 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 191 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 192 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 194 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 195 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 196 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 197 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 198 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 199 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= 201 | moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= 202 | -------------------------------------------------------------------------------- /backend/packages/controllers/facilityCtl.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "food-trucks/packages/services" 5 | "github.com/kataras/iris/v12" 6 | ) 7 | 8 | type FacilityCtl struct { 9 | C iris.Context 10 | FacilitySvc *services.FacilitySvc 11 | } 12 | 13 | func (f FacilityCtl) GetCenter() any { 14 | return f.FacilitySvc.Center 15 | } 16 | 17 | func (f FacilityCtl) Get(qry struct { 18 | Lat float64 `url:"lat"` 19 | Lon float64 `url:"lon"` 20 | Radius float64 `url:"radius"` 21 | }) any { 22 | items, err := f.FacilitySvc.GetByLocation(f.C.Request().Context(), qry.Lat, qry.Lon, qry.Radius) 23 | if err != nil { 24 | return err 25 | } 26 | return items 27 | } 28 | 29 | func (f FacilityCtl) GetBy(id string) any { 30 | item, err := f.FacilitySvc.GetByItem(f.C.Request().Context(), id) 31 | if err != nil { 32 | return err 33 | } 34 | return item 35 | } 36 | -------------------------------------------------------------------------------- /backend/packages/models/facility.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Facility struct { 4 | LocationID string `json:"locationID"` 5 | Applicant string `json:"applicant"` 6 | FacilityType string `json:"facilityType"` 7 | CNN string `json:"cnn"` 8 | LocationDescription string `json:"locationDescription"` 9 | Address string `json:"address"` 10 | BlockLot string `json:"blockLot"` 11 | Block string `json:"block"` 12 | Lot string `json:"lot"` 13 | Permit string `json:"permit"` 14 | Status string `json:"status"` 15 | FoodItems string `json:"foodItems"` 16 | X float64 `json:"x"` 17 | Y float64 `json:"y"` 18 | Latitude float64 `json:"latitude"` 19 | Longitude float64 `json:"longitude"` 20 | Schedule string `json:"schedule"` 21 | DaysHours string `json:"daysHours"` 22 | NOISent string `json:"NOISent"` 23 | Approved string `json:"approved"` 24 | Received string `json:"received"` 25 | PriorPermit string `json:"priorPermit"` 26 | ExpirationDate string `json:"expirationDate"` 27 | Location string `json:"location"` 28 | FirePreventionDistricts string `json:"firePreventionDistricts"` 29 | PoliceDistricts string `json:"policeDistricts"` 30 | SupervisorDistricts string `json:"supervisorDistricts"` 31 | ZipCodes string `json:"zipCodes"` 32 | NeighborhoodsOld string `json:"neighborhoodsOld"` 33 | } 34 | 35 | func GetFacilityLocation(f Facility) (float64, float64) { 36 | return f.Latitude, f.Longitude 37 | } 38 | 39 | func GetFacilityKey(f Facility) string { 40 | return f.LocationID 41 | } 42 | 43 | func GetFacilityScore(f Facility) float64 { 44 | return 0 45 | } 46 | -------------------------------------------------------------------------------- /backend/packages/services/facilitySvc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "fmt" 7 | "food-trucks/packages/models" 8 | "food-trucks/packages/util/errs" 9 | "os" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | type Location struct { 15 | Lat float64 16 | Lon float64 17 | } 18 | type FacilitySvc struct { 19 | FacilityStore FacilityStore 20 | ItemFacilityStore ItemFacilityStore 21 | GeoFacilityStore GeoFacilityStore 22 | Center Location 23 | } 24 | 25 | func (t *FacilitySvc) GetByID(ctx context.Context, id string) (models.Facility, error) { 26 | var ret models.Facility 27 | items, err := t.FacilityStore.Get(ctx, []string{id}) 28 | if err != nil { 29 | return ret, err 30 | } 31 | if len(items) == 0 { 32 | return ret, fmt.Errorf("not found facility " + id) 33 | } 34 | return items[0], nil 35 | } 36 | 37 | func (t *FacilitySvc) GetByItem(ctx context.Context, item string) ([]models.Facility, error) { 38 | return t.ItemFacilityStore.GetAllMemberEntities(ctx, strings.TrimSpace(item)) 39 | } 40 | 41 | func (t *FacilitySvc) GetByLocation(ctx context.Context, lat, lon, radius float64) ([]models.Facility, error) { 42 | if lon == 0 || lat == 0 && radius == 0 { 43 | lat = t.Center.Lat 44 | lon = t.Center.Lon 45 | radius = 1 46 | } 47 | return t.GeoFacilityStore.Get(ctx, lat, lon, radius) 48 | } 49 | 50 | func (t *FacilitySvc) Seed(p string) error { 51 | ctx := context.Background() 52 | facilities, err := t.readCSV(p) 53 | if err != nil { 54 | return errs.Errf("Fail to read csv %w", err) 55 | } 56 | t.getCenter(facilities) 57 | if err = t.cacheFacilities(ctx, facilities); err != nil { 58 | return errs.Errf("failed to cache facilities, %w", err) 59 | } 60 | if err = t.cacheFoodItems(ctx, facilities); err != nil { 61 | return errs.Errf("failed to cache food items, %w", err) 62 | } 63 | return t.cacheLocations(ctx, facilities) 64 | } 65 | 66 | func (t *FacilitySvc) getCenter(facilities []models.Facility) { 67 | var lon, lat float64 68 | for _, facility := range facilities { 69 | lon += facility.Longitude 70 | lat += facility.Latitude 71 | } 72 | t.Center.Lat = lat / float64(len(facilities)) 73 | t.Center.Lon = lon / float64(len(facilities)) 74 | } 75 | 76 | func (t *FacilitySvc) cacheLocations(ctx context.Context, facilities []models.Facility) error { 77 | for _, facility := range facilities { 78 | if err := t.GeoFacilityStore.Add(ctx, facility); err != nil { 79 | return errs.Errf("Fail to seed location data", err) 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | func (t *FacilitySvc) cacheFoodItems(ctx context.Context, facilities []models.Facility) error { 86 | for _, facility := range facilities { 87 | for _, item := range strings.Split(facility.FoodItems, ":") { 88 | item = strings.TrimSpace(item) 89 | if err := t.ItemFacilityStore.AddMem(ctx, item, []models.Facility{facility}); err != nil { 90 | return errs.Errf("Fail to seed food items, %w", err) 91 | } 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | func (t *FacilitySvc) cacheFacilities(ctx context.Context, facilities []models.Facility) error { 98 | return t.FacilityStore.Set(ctx, facilities) 99 | } 100 | 101 | func (t *FacilitySvc) readCSV(p string) ([]models.Facility, error) { 102 | file, err := os.Open(p) 103 | if err != nil { 104 | return nil, err 105 | } 106 | defer file.Close() 107 | reader := csv.NewReader(file) 108 | reader.FieldsPerRecord = -1 // allows variable number of fields per record 109 | records, err := reader.ReadAll() 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | var facilities []models.Facility 115 | 116 | // Skip the header row 117 | for i, record := range records { 118 | if i == 0 { 119 | continue 120 | } 121 | 122 | x, _ := strconv.ParseFloat(record[12], 64) 123 | y, _ := strconv.ParseFloat(record[13], 64) 124 | latitude, _ := strconv.ParseFloat(record[14], 64) 125 | longitude, _ := strconv.ParseFloat(record[15], 64) 126 | 127 | facility := models.Facility{ 128 | LocationID: record[0], 129 | Applicant: record[1], 130 | FacilityType: record[2], 131 | CNN: record[3], 132 | LocationDescription: record[4], 133 | Address: record[5], 134 | BlockLot: record[6], 135 | Block: record[7], 136 | Lot: record[8], 137 | Permit: record[9], 138 | Status: record[10], 139 | FoodItems: record[11], 140 | X: x, 141 | Y: y, 142 | Latitude: latitude, 143 | Longitude: longitude, 144 | Schedule: record[16], 145 | DaysHours: record[17], 146 | NOISent: record[18], 147 | Approved: record[19], 148 | Received: record[20], 149 | PriorPermit: record[21], 150 | ExpirationDate: record[22], 151 | Location: record[23], 152 | FirePreventionDistricts: record[24], 153 | PoliceDistricts: record[25], 154 | SupervisorDistricts: record[26], 155 | ZipCodes: record[27], 156 | NeighborhoodsOld: record[28], 157 | } 158 | if facility.Longitude > 0 || facility.Latitude > 0 { 159 | facilities = append(facilities, facility) 160 | } 161 | } 162 | 163 | return facilities, nil 164 | } 165 | -------------------------------------------------------------------------------- /backend/packages/services/facilitySvc_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "food-trucks/packages/models" 7 | "food-trucks/packages/util/rdb" 8 | "testing" 9 | ) 10 | 11 | func TestFacilitySvc_GetByLocation(t *testing.T) { 12 | ctx := context.Background() 13 | svc := mustInit() 14 | items, err := svc.GetByLocation(ctx, 37.805885350100986, -122.41594524663745, 1) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | fmt.Println(items) 19 | } 20 | 21 | func mustInit() *FacilitySvc { 22 | config := rdb.Config{} 23 | facilityStore := rdb.NewEntityStore[string, models.Facility]("facility", 0, config). 24 | WithGetKey(models.GetFacilityKey) 25 | itemFacilityStore := rdb.NewSliceStore[string, models.Facility]("item", 0, config, facilityStore). 26 | WithGetKey(models.GetFacilityKey).WithGetScore(models.GetFacilityScore) 27 | geoFacilityStore := rdb.NewGeoStore[string, models.Facility]("geo", 0, config, facilityStore). 28 | WithGetKey(models.GetFacilityKey).WithGetLocation(models.GetFacilityLocation) 29 | facilitySvc := &FacilitySvc{ 30 | FacilityStore: facilityStore, 31 | ItemFacilityStore: itemFacilityStore, 32 | GeoFacilityStore: geoFacilityStore, 33 | } 34 | err := facilitySvc.Seed("../..//configs/data.csv") 35 | if err != nil { 36 | panic(err) 37 | } 38 | return facilitySvc 39 | } 40 | -------------------------------------------------------------------------------- /backend/packages/services/interfaces.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "food-trucks/packages/models" 6 | ) 7 | 8 | type FacilityStore interface { 9 | Set(ctx context.Context, vals []models.Facility) error 10 | Get(ctx context.Context, keys []string) ([]models.Facility, error) 11 | } 12 | 13 | type ItemFacilityStore interface { 14 | AddMem(ctx context.Context, sliceID any, items []models.Facility) error 15 | GetAllMemberEntities(ctx context.Context, sliceID any) ([]models.Facility, error) 16 | } 17 | 18 | type GeoFacilityStore interface { 19 | Add(ctx context.Context, item models.Facility) error 20 | Get(ctx context.Context, lat float64, lon float64, radius float64) ([]models.Facility, error) 21 | } 22 | -------------------------------------------------------------------------------- /backend/packages/util/errs/errs.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "runtime" 7 | ) 8 | 9 | func Err(err error) error { 10 | if err == nil { 11 | return nil 12 | } 13 | _, file, line, _ := runtime.Caller(1) 14 | _, fileName := path.Split(file) 15 | return fmt.Errorf("%s:%d %w", fileName, line, err) 16 | } 17 | 18 | func Errf(format string, args ...any) error { 19 | _, file, line, _ := runtime.Caller(1) 20 | _, fileName := path.Split(file) 21 | args = append([]any{fileName, line}, args...) 22 | return fmt.Errorf("%s:%d "+format, args...) 23 | } 24 | -------------------------------------------------------------------------------- /backend/packages/util/irisbase/irisbase.go: -------------------------------------------------------------------------------- 1 | package irisbase 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/google/uuid" 7 | "github.com/kataras/iris/v12" 8 | "github.com/kataras/iris/v12/middleware/cors" 9 | irisLogger "github.com/kataras/iris/v12/middleware/logger" 10 | "github.com/kataras/iris/v12/middleware/recover" 11 | "github.com/kataras/iris/v12/mvc" 12 | "github.com/samber/lo" 13 | ) 14 | 15 | type AppConfig struct { 16 | ApiPrefix string `yaml:"apiPrefix"` 17 | Debug bool `yaml:"debug"` 18 | Port int `yaml:"port"` 19 | } 20 | 21 | type errHandler func(ctx iris.Context, err error) 22 | type App struct { 23 | Config AppConfig 24 | IrisApp *iris.Application 25 | ErrHandler errHandler 26 | } 27 | 28 | type AppBuilder interface { 29 | BadRequest() []error 30 | Services() []any 31 | Controller() map[string]any 32 | } 33 | 34 | func createErrorHandler(isDebug bool, badRequests []error) errHandler { 35 | return func(ctx iris.Context, err error) { 36 | id := uuid.New().ID() 37 | code := 500 38 | for _, err2 := range badRequests { 39 | if errors.As(err, &err2) { 40 | code = 400 41 | break 42 | } 43 | } 44 | 45 | if isDebug { 46 | ctx.StopWithError(code, fmt.Errorf("%w, id=%v", err, id)) 47 | } else { 48 | if code == 400 { 49 | ctx.StopWithError(code, fmt.Errorf("bad request, id=%v", id)) 50 | } else { 51 | ctx.StopWithError(code, fmt.Errorf("internal error, id=%v", id)) 52 | } 53 | } 54 | fmt.Printf("API-ERR: %v, ErrID=%v\n", err, id) 55 | } 56 | } 57 | 58 | func NewIrisApp(config AppConfig, builder AppBuilder) *App { 59 | i := &App{ 60 | Config: config, 61 | } 62 | i.ErrHandler = createErrorHandler(config.Debug, builder.BadRequest()) 63 | app := iris.New() 64 | app.Use(recover.New()) 65 | app.Use(irisLogger.New()) 66 | if i.Config.Debug { 67 | fmt.Println("************************") 68 | fmt.Println("running on debug mode...") 69 | fmt.Println("************************") 70 | app.Logger().SetLevel("debug") 71 | app.UseRouter(cors.New(). 72 | HandleErrorFunc(i.ErrHandler). 73 | ExtractOriginFunc(cors.DefaultOriginExtractor). 74 | ReferrerPolicy(cors.NoReferrerWhenDowngrade). 75 | AllowOrigin("*"). 76 | AllowHeaders("content-type, authorization"). 77 | Handler()) 78 | } 79 | app.RegisterDependency(lo.ToAnySlice(builder.Services())...) 80 | for s, a := range builder.Controller() { 81 | mvc.New(app.Party(i.Config.ApiPrefix + s)).Handle(a).HandleError(i.ErrHandler) 82 | } 83 | app.HandleDir("/", iris.Dir("./web"), iris.DirOptions{ 84 | IndexName: "index.html", 85 | SPA: true, 86 | }) 87 | i.IrisApp = app 88 | return i 89 | } 90 | 91 | func (i *App) AddRouter(relPath string, handler any) { 92 | mvc.New(i.IrisApp.Party(i.Config.ApiPrefix + relPath)).Handle(handler).HandleError(i.ErrHandler) 93 | } 94 | 95 | func (i *App) Start() { 96 | if err := i.IrisApp.Listen(fmt.Sprintf(":%d", i.Config.Port), func(app *iris.Application) { 97 | app.Configure( 98 | iris.WithOptimizations, 99 | iris.WithFireMethodNotAllowed, 100 | iris.WithPathIntelligence, 101 | ) 102 | }); err != nil { 103 | panic(err) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/basic.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | /* 9 | performance is not optimized and not scalable, just for simple use cases; 10 | for advance usage, e.g. multiple redis shard and batch operation for better performance, use entityStore 11 | */ 12 | var client *Client[string] 13 | 14 | func Init(config Config) { 15 | client = NewClient[string](config) 16 | } 17 | 18 | func GetStr(ctx context.Context, namespace string, key string) (string, error) { 19 | return client.get(ctx, namespace, key) 20 | } 21 | 22 | func SetStr(ctx context.Context, namespace string, key string, value string, expiration time.Duration) error { 23 | return client.set(ctx, namespace, key, value, expiration) 24 | } 25 | 26 | func Del(ctx context.Context, namespace string, key string) error { 27 | return client.del(ctx, namespace, key) 28 | } 29 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/basic_test.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestSetStr(t *testing.T) { 11 | Init(getConfig()) 12 | err := SetStr(context.Background(), "user", "jk", "2002 JK ", time.Hour) 13 | if err != nil { 14 | t.Fatal() 15 | } 16 | } 17 | 18 | func TestGetStr(t *testing.T) { 19 | Init(getConfig()) 20 | s, err := GetStr(context.Background(), "user", "jk") 21 | if IgnoreNoKey(err) != nil { 22 | t.Fatal(err) 23 | } 24 | fmt.Println(s) 25 | } 26 | 27 | func TestDel(t *testing.T) { 28 | Init(getConfig()) 29 | err := Del(context.Background(), "user", "jk") 30 | if err != nil { 31 | t.Fatal() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/client.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "food-trucks/packages/util/safeslice" 7 | "github.com/redis/go-redis/v9" 8 | "github.com/samber/lo" 9 | "golang.org/x/exp/constraints" 10 | "golang.org/x/sync/errgroup" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type Config struct { 16 | Addr string `yaml:"addr"` 17 | Prefix string `yaml:"prefix"` 18 | HashtagPosition int `yaml:"hashtagPosition"` 19 | Gzip bool `yaml:"gzip"` 20 | Enabled bool `yaml:"enabled"` 21 | } 22 | 23 | type Client[K constraints.Ordered] struct { 24 | Config 25 | Client redis.UniversalClient 26 | } 27 | 28 | func NewClient[K constraints.Ordered](config Config) *Client[K] { 29 | return &Client[K]{ 30 | Config: config, 31 | Client: redis.NewUniversalClient(&redis.UniversalOptions{Addrs: strings.Split(config.Addr, ",")}), 32 | } 33 | } 34 | 35 | func (c *Client[K]) makeKey(namespace string, id K) string { 36 | return fmt.Sprintf("%s:%s:%v", c.Prefix, namespace, id) 37 | } 38 | 39 | func (c *Client[K]) mDel(ctx context.Context, namespace string, ids []K) error { 40 | g, ctx := errgroup.WithContext(ctx) 41 | for _, entities := range c.tagKeys(namespace, ids) { 42 | g.Go(func() error { 43 | keys := lo.Map(entities, func(item lo.Entry[string, K], index int) string { 44 | return item.Key 45 | }) 46 | _, err := c.Client.Del(ctx, keys...).Result() 47 | return err 48 | }) 49 | } 50 | return g.Wait() 51 | } 52 | 53 | func (c *Client[K]) del(ctx context.Context, namespace string, id K) error { 54 | _, err := c.Client.Del(ctx, c.makeKey(namespace, id)).Result() 55 | return err 56 | } 57 | 58 | func (c *Client[K]) mGet(ctx context.Context, namespace string, ids []K) ([]string, []K, error) { 59 | g, ctx := errgroup.WithContext(ctx) 60 | ret := safeslice.NewSafeSlice[string]() 61 | missed := safeslice.NewSafeSlice[K]() 62 | for _, entities := range c.tagKeys(namespace, ids) { 63 | g.Go(func() error { 64 | keys := lo.Map(entities, func(item lo.Entry[string, K], index int) string { 65 | return item.Key 66 | }) 67 | 68 | res, err := c.Client.MGet(ctx, keys...).Result() 69 | if err != nil { 70 | return err 71 | } 72 | 73 | var vals []string 74 | for i, re := range res { 75 | if re == nil { 76 | missed.Append(entities[i].Value) 77 | } else { 78 | vals = append(vals, re.(string)) 79 | } 80 | } 81 | 82 | if c.Gzip { 83 | var err error 84 | vals, err = munzip(vals) 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | ret.Append(vals...) 90 | return nil 91 | }) 92 | } 93 | if err := g.Wait(); err != nil { 94 | return nil, nil, err 95 | } 96 | return ret.All(), missed.All(), nil 97 | } 98 | 99 | func (c *Client[K]) get(ctx context.Context, namespace string, id K) (string, error) { 100 | str, err := c.Client.Get(ctx, c.makeKey(namespace, id)).Result() 101 | if err != nil { 102 | return "", err 103 | } 104 | if c.Gzip { 105 | str, err = unzip(str) 106 | if err != nil { 107 | return "", err 108 | } 109 | } 110 | return str, nil 111 | } 112 | 113 | func (c *Client[K]) mSet(ctx context.Context, namespace string, duration time.Duration, entities []lo.Entry[K, string]) error { 114 | g, ctx := errgroup.WithContext(ctx) 115 | if c.Gzip { 116 | for i, entity := range entities { 117 | var err error 118 | entity.Value, err = zip(entity.Value) 119 | if err != nil { 120 | return err 121 | } 122 | entities[i] = entity 123 | } 124 | } 125 | for _, items := range c.tagEntities(namespace, entities) { 126 | g.Go(func() error { 127 | p := c.Client.Pipeline() 128 | for _, entity := range items { 129 | p.Set(ctx, entity.Key, entity.Value, duration) 130 | } 131 | _, err := p.Exec(ctx) 132 | return err 133 | }) 134 | } 135 | return g.Wait() 136 | } 137 | 138 | func (c *Client[K]) set(ctx context.Context, namespace string, id K, value string, expiration time.Duration) error { 139 | var err error 140 | if c.Gzip { 141 | value, err = zip(value) 142 | if err != nil { 143 | return err 144 | } 145 | } 146 | _, err = c.Client.Set(ctx, c.makeKey(namespace, id), value, expiration).Result() 147 | return err 148 | } 149 | 150 | type TagKey struct { 151 | Tag string 152 | Key string 153 | } 154 | 155 | func (c *Client[K]) tag(namespace string, id K) TagKey { 156 | str := c.makeKey(namespace, id) 157 | ret := TagKey{Key: str} 158 | if len(str) > c.HashtagPosition { 159 | pos := len(str) - c.HashtagPosition 160 | ret.Tag = "{" + str[:pos] + "}" 161 | ret.Key = ret.Tag + str[pos:] 162 | } 163 | return ret 164 | } 165 | 166 | func (c *Client[K]) tagEntities(namespace string, entities []lo.Entry[K, string]) map[string][]lo.Entry[string, string] { 167 | ret := make(map[string][]lo.Entry[string, string]) 168 | for _, entity := range entities { 169 | tag := c.tag(namespace, entity.Key) 170 | ret[tag.Tag] = append(ret[tag.Tag], lo.Entry[string, string]{Key: tag.Key, Value: entity.Value}) 171 | } 172 | return ret 173 | } 174 | 175 | func (c *Client[K]) tagKeys(namespace string, ids []K) map[string][]lo.Entry[string, K] { 176 | ret := make(map[string][]lo.Entry[string, K]) 177 | for _, id := range ids { 178 | tag := c.tag(namespace, id) 179 | ret[tag.Tag] = append(ret[tag.Tag], lo.Entry[string, K]{ 180 | Key: tag.Key, 181 | Value: id, 182 | }) 183 | } 184 | return ret 185 | } 186 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/client_test.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/samber/lo" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // start redis from local 12 | // docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest 13 | // docker exec -it redis-stack redis-cli 14 | 15 | func getConfig() Config { 16 | return Config{ 17 | Addr: "127.0.0.1:6379", 18 | Prefix: "Test", 19 | HashtagPosition: 3, 20 | Gzip: false, 21 | Enabled: true, 22 | } 23 | } 24 | func getIntKeyClient() *Client[int] { 25 | return NewClient[int](getConfig()) 26 | } 27 | 28 | func TestMSet(t *testing.T) { 29 | c := getIntKeyClient() 30 | err := c.mSet(context.Background(), "posts", time.Hour, []lo.Entry[int, string]{ 31 | { 32 | Key: 10001, 33 | Value: "v10001", 34 | }, 35 | { 36 | Key: 10002, 37 | Value: "v10002", 38 | }, 39 | { 40 | Key: 10003, 41 | Value: "v10003", 42 | }, 43 | }) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | 49 | func TestMGet(t *testing.T) { 50 | c := getIntKeyClient() 51 | vals, missed, err := c.mGet(context.Background(), "posts", []int{10001, 10008}) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | fmt.Println(vals, missed) 56 | } 57 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/di_test.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type TestEntity string 11 | type EntityCache interface { 12 | Del(ctx context.Context, ids ...int) error 13 | Set(ctx context.Context, vals []TestEntity) error 14 | Get(ctx context.Context, keys []int) ([]TestEntity, error) 15 | GetFetch(ctx context.Context, keys []int, fetch func([]int) ([]TestEntity, error)) ([]TestEntity, error, error) 16 | GetFetchSet(ctx context.Context, keys []int, fetch func([]int) ([]TestEntity, error)) ([]TestEntity, error, error) 17 | } 18 | 19 | type SliceCache interface { 20 | SetSlice(ctx context.Context, sliceID any, items []TestEntity, left float64) error 21 | DelMember(ctx context.Context, sliceID any, memberKeys []int) error 22 | RevGetSlice(ctx context.Context, sliceID any, score *float64, count int) ([]TestEntity, float64, error) 23 | RevTruncate(ctx context.Context, sliceID any, score float64) error 24 | } 25 | 26 | type TimeSliceCache interface { 27 | GetLatest(ctx context.Context, sliceID any, count int, fetch func() ([]TestEntity, error)) ([]TestEntity, error, error) 28 | GetByTime(ctx context.Context, sliceID any, ts time.Time, count int, fetch func(ts time.Time, count int) ([]TestEntity, error)) ([]TestEntity, error) 29 | TruncateExpired(ctx context.Context, sliceID any) error 30 | } 31 | 32 | type TestService struct { 33 | entityCache EntityCache 34 | sliceCache SliceCache 35 | timeSliceCache TimeSliceCache 36 | } 37 | 38 | func NewTestService(entity EntityCache, slice SliceCache, timeSlice TimeSliceCache) *TestService { 39 | return &TestService{ 40 | entityCache: entity, 41 | sliceCache: slice, 42 | timeSliceCache: timeSlice, 43 | } 44 | } 45 | 46 | // test entity, slice, timeSlice can be injected to service 47 | func TestNewTestService(t *testing.T) { 48 | entityStore := NewEntityStore[int, TestEntity]("testEntities", time.Minute*60, getConfig()) 49 | sliceStore := NewSliceStore[int, TestEntity]("testEntities", time.Minute*60, getConfig(), entityStore) 50 | timeSliceStore := NewTimeSliceStore[int, TestEntity]("testEntities", time.Minute*60, time.Minute, getConfig(), entityStore) 51 | service := NewTestService(entityStore, sliceStore, timeSliceStore) 52 | fmt.Println(service) 53 | } 54 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/entityStore.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "food-trucks/packages/util/singleflight" 8 | "github.com/samber/lo" 9 | "golang.org/x/exp/constraints" 10 | "time" 11 | ) 12 | 13 | type EntityStore[K constraints.Ordered, V any] struct { 14 | client *Client[K] 15 | namespace string 16 | entityDuration time.Duration 17 | getKey func(V) K 18 | single *singleflight.Group[string, []V] 19 | } 20 | 21 | type gGetResult[K constraints.Ordered, V any] struct { 22 | Values []V 23 | Missed []K 24 | } 25 | 26 | func NewEntityStore[K constraints.Ordered, V any](namespace string, duration time.Duration, config Config) *EntityStore[K, V] { 27 | return &EntityStore[K, V]{ 28 | single: &singleflight.Group[string, []V]{}, 29 | client: NewClient[K](config), 30 | namespace: namespace, 31 | entityDuration: duration, 32 | } 33 | } 34 | 35 | func (c *EntityStore[K, V]) WithGetKey(f func(V) K) *EntityStore[K, V] { 36 | c.getKey = f 37 | return c 38 | } 39 | 40 | func (c *EntityStore[K, V]) Set(ctx context.Context, vals []V) error { 41 | items, err := c.toStrEntity(vals) 42 | if err != nil { 43 | return err 44 | } 45 | return c.client.mSet(ctx, c.namespace, c.entityDuration, items) 46 | } 47 | 48 | func (c *EntityStore[K, V]) Del(ctx context.Context, ids ...K) error { 49 | return c.client.mDel(ctx, c.namespace, ids) 50 | } 51 | 52 | func (c *EntityStore[K, V]) Get(ctx context.Context, keys []K) ([]V, error) { 53 | return c.getFetchSet(ctx, keys, false, nil) 54 | } 55 | 56 | func (c *EntityStore[K, V]) GetFetch(ctx context.Context, keys []K, 57 | fetch func([]K) ([]V, error), 58 | ) ([]V, error, error) { 59 | singleKey := fmt.Sprintf("%v", keys) 60 | vals, err, _ := c.single.Do(singleKey, func() ([]V, error) { 61 | vals, err := c.getFetchSet(ctx, keys, false, fetch) 62 | return vals, err 63 | }) 64 | return errOrWarning(vals, err) 65 | } 66 | 67 | func (c *EntityStore[K, V]) GetFetchSet(ctx context.Context, keys []K, 68 | fetch func([]K) ([]V, error), 69 | ) ([]V, error, error) { 70 | singleKey := fmt.Sprintf("S:%v", keys) 71 | vals, err, _ := c.single.Do(singleKey, func() ([]V, error) { 72 | return c.getFetchSet(ctx, keys, true, fetch) 73 | }) 74 | return errOrWarning(vals, err) 75 | } 76 | 77 | func (c *EntityStore[K, V]) toStrEntity(values []V) ([]lo.Entry[K, string], error) { 78 | if c.getKey == nil { 79 | return nil, errors.New("getKey not set") 80 | } 81 | var ret []lo.Entry[K, string] 82 | for _, value := range values { 83 | str, err := toStr(value) 84 | if err != nil { 85 | return nil, err 86 | } 87 | ret = append(ret, lo.Entry[K, string]{ 88 | Key: c.getKey(value), 89 | Value: str, 90 | }) 91 | } 92 | return ret, nil 93 | } 94 | 95 | func (c *EntityStore[K, V]) get(ctx context.Context, keys []K) (gGetResult[K, V], error) { 96 | ret := gGetResult[K, V]{} 97 | items, missed, err := c.client.mGet(ctx, c.namespace, keys) 98 | if err != nil { 99 | return ret, nil 100 | } 101 | vals, err := mFromStr[V](items) 102 | if err != nil { 103 | return ret, nil 104 | } 105 | ret.Missed = missed 106 | ret.Values = vals 107 | return ret, nil 108 | } 109 | 110 | func (c *EntityStore[K, V]) getFetchSet(ctx context.Context, keys []K, cacheFetchResult bool, 111 | fetch func([]K) ([]V, error), 112 | ) ([]V, error) { 113 | if c.getKey == nil { 114 | return nil, errors.New("getKey not set") 115 | } 116 | 117 | ret, err := c.get(ctx, keys) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | var warning error 123 | if len(ret.Missed) > 0 && fetch != nil { 124 | items, err := fetch(ret.Missed) 125 | if err != nil { 126 | return nil, err 127 | } 128 | ret.Values = append(ret.Values, items...) 129 | 130 | if cacheFetchResult { 131 | warning = wrapSetCacheError(c.Set(ctx, items)) 132 | } 133 | } 134 | return c.sort(keys, ret.Values), warning 135 | } 136 | 137 | func (c *EntityStore[K, V]) sort(keys []K, vals []V) []V { 138 | return lo.Map(keys, func(k K, index int) V { 139 | item, _ := lo.Find(vals, func(v V) bool { 140 | return k == c.getKey(v) 141 | }) 142 | return item 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/entityStore_test.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/samber/lo" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | type EntityStorePost struct { 12 | ID int 13 | Title string 14 | } 15 | 16 | func EntityStorePostID(p EntityStorePost) int { 17 | return p.ID 18 | } 19 | 20 | var TestEntityStore = "TestEntityStore" 21 | 22 | func TestEntityStore_Set(t *testing.T) { 23 | ctx := context.Background() 24 | entityStore := NewEntityStore[int, EntityStorePost](TestEntityStore, time.Hour, getConfig()). 25 | WithGetKey(EntityStorePostID) 26 | if err := entityStore.Set(ctx, fakeEntityStorePost(10)); err != nil { 27 | t.Fatal(err) 28 | } 29 | } 30 | 31 | func TestEntityStore_Del(t *testing.T) { 32 | ctx := context.Background() 33 | entityStore := NewEntityStore[int, EntityStorePost](TestEntityStore, time.Hour, getConfig()). 34 | WithGetKey(EntityStorePostID) 35 | if err := entityStore.Del(ctx, 1001, 1002, 1003, 1004, 1005, 1007); err != nil { 36 | t.Fatal(err) 37 | } 38 | } 39 | 40 | func TestEntityStore_Get(t *testing.T) { 41 | ctx := context.Background() 42 | entityStore := NewEntityStore[int, EntityStorePost](TestEntityStore, time.Hour, getConfig()). 43 | WithGetKey(EntityStorePostID) 44 | if items, err := entityStore.Get(ctx, fakeEntityStoreIDs(10)); err != nil { 45 | t.Fatal(err) 46 | } else { 47 | fmt.Println(items) 48 | } 49 | } 50 | 51 | func TestEntityStore_GetFetch(t *testing.T) { 52 | ctx := context.Background() 53 | entityStore := NewEntityStore[int, EntityStorePost](TestEntityStore, time.Hour, getConfig()). 54 | WithGetKey(EntityStorePostID) 55 | if items, err, _ := entityStore.GetFetch(ctx, fakeEntityStoreIDs(10), fakeEntityPostByIDs); err != nil { 56 | t.Fatal(err) 57 | } else { 58 | fmt.Println(items) 59 | } 60 | } 61 | 62 | func TestEntityStore_GetFetchSet(t *testing.T) { 63 | ctx := context.Background() 64 | entityStore := NewEntityStore[int, EntityStorePost](TestEntityStore, time.Hour, getConfig()). 65 | WithGetKey(EntityStorePostID) 66 | if items, err, _ := entityStore.GetFetchSet(ctx, fakeEntityStoreIDs(10), fakeEntityPostByIDs); err != nil { 67 | t.Fatal(err) 68 | } else { 69 | fmt.Println(items) 70 | } 71 | } 72 | 73 | func fakeEntityPostByIDs(ints []int) ([]EntityStorePost, error) { 74 | return lo.Map(ints, func(item int, index int) EntityStorePost { 75 | return EntityStorePost{ 76 | ID: item, 77 | Title: fmt.Sprintf("fetch again %v", item), 78 | } 79 | }), nil 80 | 81 | } 82 | 83 | func fakeEntityStorePost(count int) []EntityStorePost { 84 | var ret []EntityStorePost 85 | for i := 0; i < count; i++ { 86 | ret = append(ret, EntityStorePost{ 87 | ID: 1000 + i, 88 | Title: fmt.Sprintf("Post %v", i), 89 | }, 90 | ) 91 | } 92 | return ret 93 | } 94 | 95 | func fakeEntityStoreIDs(count int) []int { 96 | var ret []int 97 | for i := 0; i < count; i++ { 98 | ret = append(ret, 1000+i) 99 | } 100 | return ret 101 | } 102 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/geoStore.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | "golang.org/x/exp/constraints" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type GeoStore[K constraints.Ordered, Entity any] struct { 13 | Config 14 | namespace string 15 | duration time.Duration 16 | client redis.UniversalClient 17 | entityStore Cacheable[K, Entity] 18 | getMemberKey func(Entity) K 19 | getLocation func(Entity) (float64, float64) 20 | } 21 | 22 | func NewGeoStore[MemberKey constraints.Ordered, Entity any]( 23 | namespace string, 24 | duration time.Duration, 25 | config Config, 26 | entityStore Cacheable[MemberKey, Entity], 27 | ) *GeoStore[MemberKey, Entity] { 28 | return &GeoStore[MemberKey, Entity]{ 29 | Config: config, 30 | client: redis.NewUniversalClient(&redis.UniversalOptions{Addrs: strings.Split(config.Addr, ",")}), 31 | namespace: namespace, 32 | entityStore: entityStore, 33 | duration: duration, 34 | } 35 | } 36 | 37 | func (s *GeoStore[K, V]) WithGetKey(f func(V) K) *GeoStore[K, V] { 38 | s.getMemberKey = f 39 | return s 40 | } 41 | 42 | func (s *GeoStore[K, V]) WithGetLocation(f func(V) (lat float64, lon float64)) *GeoStore[K, V] { 43 | s.getLocation = f 44 | return s 45 | } 46 | 47 | func (s *GeoStore[K, V]) Add(ctx context.Context, item V) error { 48 | lat, lon := s.getLocation(item) 49 | key := fmt.Sprintf("%v", s.getMemberKey(item)) 50 | _, err := s.client.GeoAdd(ctx, s.namespace, &redis.GeoLocation{ 51 | Name: key, 52 | Longitude: lon, 53 | Latitude: lat, 54 | }).Result() 55 | return err 56 | } 57 | 58 | func (s *GeoStore[K, V]) Get(ctx context.Context, lat float64, lon float64, radius float64) ([]V, error) { 59 | res, err := s.client.GeoRadius(ctx, s.namespace, lon, lat, &redis.GeoRadiusQuery{ 60 | Radius: radius, 61 | Unit: "km", 62 | WithCoord: true, 63 | WithDist: true, 64 | }).Result() 65 | if err != nil { 66 | return nil, err 67 | } 68 | var keys []K 69 | for _, re := range res { 70 | k, err := keyFromStar[K](re.Name) 71 | if err != nil { 72 | return nil, err 73 | } 74 | keys = append(keys, k) 75 | } 76 | return s.entityStore.Get(ctx, keys) 77 | } 78 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/interfaces.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "golang.org/x/exp/constraints" 6 | ) 7 | 8 | type Cacheable[K constraints.Ordered, V any] interface { 9 | Set(ctx context.Context, vals []V) error 10 | Get(ctx context.Context, keys []K) ([]V, error) 11 | } 12 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/sliceStore.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "food-trucks/packages/util/errs" 7 | "github.com/redis/go-redis/v9" 8 | "github.com/samber/lo" 9 | "golang.org/x/exp/constraints" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type SliceStore[K constraints.Ordered, Entity any] struct { 15 | Config 16 | namespace string 17 | duration time.Duration 18 | client redis.UniversalClient 19 | entityStore Cacheable[K, Entity] 20 | getMemberKey func(Entity) K 21 | getScore func(Entity) float64 22 | } 23 | 24 | func NewSliceStore[MemberKey constraints.Ordered, Entity any]( 25 | namespace string, 26 | duration time.Duration, 27 | config Config, 28 | entityStore Cacheable[MemberKey, Entity], 29 | ) *SliceStore[MemberKey, Entity] { 30 | return &SliceStore[MemberKey, Entity]{ 31 | Config: config, 32 | client: redis.NewUniversalClient(&redis.UniversalOptions{Addrs: strings.Split(config.Addr, ",")}), 33 | namespace: namespace, 34 | entityStore: entityStore, 35 | duration: duration, 36 | } 37 | } 38 | 39 | func (s *SliceStore[K, V]) WithGetKey(f func(V) K) *SliceStore[K, V] { 40 | s.getMemberKey = f 41 | return s 42 | } 43 | 44 | func (s *SliceStore[K, V]) WithGetScore(f func(V) float64) *SliceStore[K, V] { 45 | s.getScore = f 46 | return s 47 | } 48 | 49 | func (s *SliceStore[K, V]) DelSlice(ctx context.Context, sliceID any) error { 50 | key := s.sliceKey(sliceID) 51 | _, err := s.client.Del(ctx, key).Result() 52 | return err 53 | } 54 | 55 | func (s *SliceStore[K, V]) DelMember(ctx context.Context, sliceID any, memberKeys []K) error { 56 | key := s.sliceKey(sliceID) 57 | _, err := s.client.ZRem(ctx, key, lo.ToAnySlice(memberKeys)...).Result() 58 | return err 59 | } 60 | 61 | func (s *SliceStore[K, Entity]) AddMem(ctx context.Context, sliceID any, items []Entity) error { 62 | key := s.sliceKey(sliceID) 63 | if err := s.entityStore.Set(ctx, items); err != nil { 64 | return errs.Err(err) 65 | } 66 | members := make([]redis.Z, len(items)) 67 | for i, item := range items { 68 | members[i] = redis.Z{ 69 | Score: s.getScore(item), 70 | Member: s.getMemberKey(item), 71 | } 72 | } 73 | _, err := s.client.ZAdd(ctx, key, members...).Result() 74 | return err 75 | } 76 | 77 | func (s *SliceStore[K, Entity]) GetAllMemberEntities(ctx context.Context, sliceID any) ([]Entity, error) { 78 | option := &redis.ZRangeBy{ 79 | Min: "-inf", 80 | Max: "+inf", 81 | } 82 | key := s.sliceKey(sliceID) 83 | p := s.client.Pipeline() 84 | memCmd := p.ZRevRangeByScore(ctx, key, option) 85 | if _, err := p.Exec(ctx); IgnoreNoKey(err) != nil { 86 | return nil, errs.Err(err) 87 | } 88 | items, err := s.getEntities(ctx, memCmd.Val()) 89 | if err != nil { 90 | return nil, errs.Err(err) 91 | } 92 | 93 | return items, nil 94 | } 95 | 96 | func (s *SliceStore[K, V]) getEntities(ctx context.Context, members []string) ([]V, error) { 97 | var memberKeys []K 98 | for _, mem := range members { 99 | memberK, err := keyFromStar[K](mem) 100 | if err != nil { 101 | return nil, errs.Err(err) 102 | } 103 | memberKeys = append(memberKeys, memberK) 104 | } 105 | return s.entityStore.Get(ctx, memberKeys) 106 | } 107 | 108 | func (s *SliceStore[K, V]) sliceKey(sliceID any) string { 109 | return fmt.Sprintf("%s:%s:%v", s.Prefix, s.namespace, sliceID) 110 | } 111 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/types.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // SetCacheError most of the time we can ignore this error, this error often return as warning. 9 | var SetCacheError = errors.New("fail to set cache") 10 | 11 | func wrapSetCacheError(err error) error { 12 | if err != nil { 13 | return fmt.Errorf("%w, %v", SetCacheError, err) 14 | } 15 | return nil 16 | } 17 | 18 | func errOrWarning[V any](vals V, err error) (V, error, error) { 19 | if errors.Is(err, SetCacheError) { 20 | return vals, nil, err 21 | } else { 22 | return vals, err, nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/packages/util/rdb/util.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/binary" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "github.com/redis/go-redis/v9" 11 | "golang.org/x/exp/constraints" 12 | "io/ioutil" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | func zip(input string) (string, error) { 18 | var compressed bytes.Buffer 19 | gzipWriter := gzip.NewWriter(&compressed) 20 | _, err := gzipWriter.Write([]byte(input)) 21 | if err != nil { 22 | return "", fmt.Errorf("error gzip write %w", err) 23 | } 24 | err = gzipWriter.Close() 25 | if err != nil { 26 | return "", fmt.Errorf("error gzip write %w", err) 27 | } 28 | return string(compressed.Bytes()), nil 29 | } 30 | 31 | func munzip(vals []string) ([]string, error) { 32 | var ret []string 33 | var err error 34 | for _, val := range vals { 35 | val, err = unzip(val) 36 | if err != nil { 37 | return nil, err 38 | } 39 | ret = append(ret, val) 40 | } 41 | return ret, nil 42 | } 43 | func unzip(compressedData string) (string, error) { 44 | compressedReader := bytes.NewReader([]byte(compressedData)) 45 | gzipReader, err := gzip.NewReader(compressedReader) 46 | if err != nil { 47 | return "", fmt.Errorf("error creating gzip reader: %w", err) 48 | } 49 | decompressed, err := ioutil.ReadAll(gzipReader) 50 | if err != nil { 51 | return "", fmt.Errorf("error decompressing data: %w", err) 52 | } 53 | err = gzipReader.Close() 54 | if err != nil { 55 | return "", fmt.Errorf("error closing gzip reader: %w", err) 56 | } 57 | return string(decompressed), nil 58 | } 59 | 60 | func fromByte[V any](bs []byte) (V, error) { 61 | var v V 62 | var anyV any 63 | anyV = v 64 | switch anyV.(type) { 65 | case string: 66 | anyV = string(bs) 67 | v = anyV.(V) 68 | return v, nil 69 | case int: 70 | anyV = int(binary.LittleEndian.Uint64(bs)) 71 | v = anyV.(V) 72 | return v, nil 73 | case uint: 74 | anyV = uint(binary.LittleEndian.Uint64(bs)) 75 | v = anyV.(V) 76 | return v, nil 77 | case int64: 78 | anyV = int64(binary.LittleEndian.Uint64(bs)) 79 | v = anyV.(V) 80 | return v, nil 81 | case uint64: 82 | anyV = binary.LittleEndian.Uint64(bs) 83 | v = anyV.(V) 84 | return v, nil 85 | default: 86 | err := json.Unmarshal(bs, &v) 87 | return v, err 88 | } 89 | } 90 | 91 | func toBytes(k any) ([]byte, error) { 92 | switch i := k.(type) { 93 | case string: 94 | return []byte(i), nil 95 | case int: 96 | bs := make([]byte, 8) 97 | binary.LittleEndian.PutUint64(bs, uint64(i)) 98 | return bs, nil 99 | case uint: 100 | bs := make([]byte, 8) 101 | binary.LittleEndian.PutUint64(bs, uint64(i)) 102 | return bs, nil 103 | case int64: 104 | bs := make([]byte, 8) 105 | binary.LittleEndian.PutUint64(bs, uint64(i)) 106 | return bs, nil 107 | case uint64: 108 | bs := make([]byte, 8) 109 | binary.LittleEndian.PutUint64(bs, i) 110 | return bs, nil 111 | default: 112 | return json.Marshal(k) 113 | } 114 | } 115 | func fromStr[T any](str string) (T, error) { 116 | return fromByte[T]([]byte(str)) 117 | } 118 | 119 | func mFromStr[T any](strs []string) ([]T, error) { 120 | var ret []T 121 | for _, str := range strs { 122 | s, err := fromStr[T](str) 123 | if err != nil { 124 | return nil, err 125 | } 126 | ret = append(ret, s) 127 | } 128 | return ret, nil 129 | } 130 | 131 | func toStr(v any) (string, error) { 132 | bs, err := toBytes(v) 133 | if err != nil { 134 | return "", err 135 | } 136 | return string(bs), nil 137 | } 138 | 139 | func keyFromStar[K constraints.Ordered](i string) (K, error) { 140 | var err error 141 | var k K 142 | var a any = k 143 | switch a.(type) { 144 | case int: 145 | a, err = strconv.Atoi(i) 146 | if err != nil { 147 | return k, fmt.Errorf("unsupported type") 148 | } 149 | return a.(K), nil 150 | case int64: 151 | a, err = strconv.ParseInt(i, 10, 64) 152 | if err != nil { 153 | return k, fmt.Errorf("unsupported type") 154 | } 155 | return a.(K), nil 156 | case string: 157 | a = i 158 | return a.(K), nil 159 | default: 160 | return k, fmt.Errorf("unsupported type") 161 | } 162 | } 163 | 164 | func IgnoreNoKey(err error) error { 165 | switch { 166 | case err == nil, errors.Is(err, redis.Nil), strings.Contains(err.Error(), "key that doesn't exist"): 167 | return nil 168 | default: 169 | return err 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /backend/packages/util/safeslice/safeslice.go: -------------------------------------------------------------------------------- 1 | package safeslice 2 | 3 | import "sync" 4 | 5 | type SafeSlice[T any] struct { 6 | sync.Mutex 7 | items []T 8 | } 9 | 10 | func NewSafeSlice[T any]() *SafeSlice[T] { 11 | return &SafeSlice[T]{} 12 | } 13 | 14 | func (s *SafeSlice[T]) Append(items ...T) { 15 | s.Lock() 16 | defer s.Unlock() 17 | for _, item := range items { 18 | s.items = append(s.items, item) 19 | } 20 | } 21 | 22 | func (s *SafeSlice[T]) Get(index int) T { 23 | s.Lock() 24 | defer s.Unlock() 25 | return s.items[index] 26 | } 27 | 28 | func (s *SafeSlice[T]) All() []T { 29 | s.Lock() 30 | defer s.Unlock() 31 | return s.items 32 | } 33 | -------------------------------------------------------------------------------- /backend/packages/util/singleflight/singleflight.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tailscale Inc & AUTHORS 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | // Copyright 2013 The Go Authors. All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file. 7 | 8 | // Package singleflight provides a duplicate function call suppression 9 | // mechanism. 10 | // 11 | // This is a Tailscale fork of Go's singleflight package which has had several 12 | // homes in the past: 13 | // 14 | // - https://github.com/golang/go/commit/61d3b2db6292581fc07a3767ec23ec94ad6100d1 15 | // - https://github.com/golang/groupcache/tree/master/singleflight 16 | // - https://pkg.go.dev/golang.org/x/sync/singleflight 17 | // 18 | // This fork adds generics. 19 | package singleflight // import "tailscale.com/util/singleflight" 20 | 21 | import ( 22 | "bytes" 23 | "errors" 24 | "fmt" 25 | "runtime" 26 | "runtime/debug" 27 | "sync" 28 | ) 29 | 30 | // errGoexit indicates the runtime.Goexit was called in 31 | // the user given function. 32 | var errGoexit = errors.New("runtime.Goexit was called") 33 | 34 | // A panicError is an arbitrary value recovered from a panic 35 | // with the stack trace during the execution of given function. 36 | type panicError struct { 37 | value interface{} 38 | stack []byte 39 | } 40 | 41 | // Error implements error interface. 42 | func (p *panicError) Error() string { 43 | return fmt.Sprintf("%v\n\n%s", p.value, p.stack) 44 | } 45 | 46 | func newPanicError(v interface{}) error { 47 | stack := debug.Stack() 48 | 49 | // The first line of the stack trace is of the form "goroutine N [status]:" 50 | // but by the time the panic reaches Do the goroutine may no longer exist 51 | // and its status will have changed. Trim out the misleading line. 52 | if line := bytes.IndexByte(stack[:], '\n'); line >= 0 { 53 | stack = stack[line+1:] 54 | } 55 | return &panicError{value: v, stack: stack} 56 | } 57 | 58 | // call is an in-flight or completed singleflight.Do call 59 | type call[V any] struct { 60 | wg sync.WaitGroup 61 | 62 | // These fields are written once before the WaitGroup is done 63 | // and are only read after the WaitGroup is done. 64 | val V 65 | err error 66 | 67 | // These fields are read and written with the singleflight 68 | // mutex held before the WaitGroup is done, and are read but 69 | // not written after the WaitGroup is done. 70 | dups int 71 | chans []chan<- Result[V] 72 | } 73 | 74 | // Group represents a class of work and forms a namespace in 75 | // which units of work can be executed with duplicate suppression. 76 | type Group[K comparable, V any] struct { 77 | mu sync.Mutex // protects m 78 | m map[K]*call[V] // lazily initialized 79 | } 80 | 81 | // Result holds the results of Do, so they can be passed 82 | // on a channel. 83 | type Result[V any] struct { 84 | Val V 85 | Err error 86 | Shared bool 87 | } 88 | 89 | // Do executes and returns the results of the given function, making 90 | // sure that only one execution is in-flight for a given key at a 91 | // time. If a duplicate comes in, the duplicate caller waits for the 92 | // original to complete and receives the same results. 93 | // The return value shared indicates whether v was given to multiple callers. 94 | func (g *Group[K, V]) Do(key K, fn func() (V, error)) (v V, err error, shared bool) { 95 | g.mu.Lock() 96 | if g.m == nil { 97 | g.m = make(map[K]*call[V]) 98 | } 99 | if c, ok := g.m[key]; ok { 100 | c.dups++ 101 | g.mu.Unlock() 102 | c.wg.Wait() 103 | 104 | if e, ok := c.err.(*panicError); ok { 105 | panic(e) 106 | } else if c.err == errGoexit { 107 | runtime.Goexit() 108 | } 109 | return c.val, c.err, true 110 | } 111 | c := new(call[V]) 112 | c.wg.Add(1) 113 | g.m[key] = c 114 | g.mu.Unlock() 115 | 116 | g.doCall(c, key, fn) 117 | return c.val, c.err, c.dups > 0 118 | } 119 | 120 | // DoChan is like Do but returns a channel that will receive the 121 | // results when they are ready. 122 | // 123 | // The returned channel will not be closed. 124 | func (g *Group[K, V]) DoChan(key K, fn func() (V, error)) <-chan Result[V] { 125 | ch := make(chan Result[V], 1) 126 | g.mu.Lock() 127 | if g.m == nil { 128 | g.m = make(map[K]*call[V]) 129 | } 130 | if c, ok := g.m[key]; ok { 131 | c.dups++ 132 | c.chans = append(c.chans, ch) 133 | g.mu.Unlock() 134 | return ch 135 | } 136 | c := &call[V]{chans: []chan<- Result[V]{ch}} 137 | c.wg.Add(1) 138 | g.m[key] = c 139 | g.mu.Unlock() 140 | 141 | go g.doCall(c, key, fn) 142 | 143 | return ch 144 | } 145 | 146 | // doCall handles the single call for a key. 147 | func (g *Group[K, V]) doCall(c *call[V], key K, fn func() (V, error)) { 148 | normalReturn := false 149 | recovered := false 150 | 151 | // use double-defer to distinguish panic from runtime.Goexit, 152 | // more details see https://golang.org/cl/134395 153 | defer func() { 154 | // the given function invoked runtime.Goexit 155 | if !normalReturn && !recovered { 156 | c.err = errGoexit 157 | } 158 | 159 | g.mu.Lock() 160 | defer g.mu.Unlock() 161 | c.wg.Done() 162 | if g.m[key] == c { 163 | delete(g.m, key) 164 | } 165 | 166 | if e, ok := c.err.(*panicError); ok { 167 | // In order to prevent the waiting channels from being blocked forever, 168 | // needs to ensure that this panic cannot be recovered. 169 | if len(c.chans) > 0 { 170 | go panic(e) 171 | select {} // Keep this goroutine around so that it will appear in the crash dump. 172 | } else { 173 | panic(e) 174 | } 175 | } else if c.err == errGoexit { 176 | // Already in the process of goexit, no need to call again 177 | } else { 178 | // Normal return 179 | for _, ch := range c.chans { 180 | ch <- Result[V]{c.val, c.err, c.dups > 0} 181 | } 182 | } 183 | }() 184 | 185 | func() { 186 | defer func() { 187 | if !normalReturn { 188 | // Ideally, we would wait to take a stack trace until we've determined 189 | // whether this is a panic or a runtime.Goexit. 190 | // 191 | // Unfortunately, the only way we can distinguish the two is to see 192 | // whether the recover stopped the goroutine from terminating, and by 193 | // the time we know that, the part of the stack trace relevant to the 194 | // panic has been discarded. 195 | if r := recover(); r != nil { 196 | c.err = newPanicError(r) 197 | } 198 | } 199 | }() 200 | 201 | c.val, c.err = fn() 202 | normalReturn = true 203 | }() 204 | 205 | if !normalReturn { 206 | recovered = true 207 | } 208 | } 209 | 210 | // Forget tells the singleflight to forget about a key. Future calls 211 | // to Do for this key will call the function rather than waiting for 212 | // an earlier call to complete. 213 | func (g *Group[K, V]) Forget(key K) { 214 | g.mu.Lock() 215 | delete(g.m, key) 216 | g.mu.Unlock() 217 | } 218 | -------------------------------------------------------------------------------- /backend/packages/util/singleflight/singleflight_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tailscale Inc & AUTHORS 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | // Copyright 2013 The Go Authors. All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file. 7 | 8 | package singleflight 9 | 10 | import ( 11 | "bytes" 12 | "errors" 13 | "fmt" 14 | "os" 15 | "os/exec" 16 | "runtime" 17 | "runtime/debug" 18 | "strings" 19 | "sync" 20 | "sync/atomic" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestDo(t *testing.T) { 26 | var g Group[string, any] 27 | v, err, _ := g.Do("key", func() (interface{}, error) { 28 | return "bar", nil 29 | }) 30 | if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { 31 | t.Errorf("Do = %v; want %v", got, want) 32 | } 33 | if err != nil { 34 | t.Errorf("Do error = %v", err) 35 | } 36 | } 37 | 38 | func TestDoErr(t *testing.T) { 39 | var g Group[string, any] 40 | someErr := errors.New("Some error") 41 | v, err, _ := g.Do("key", func() (interface{}, error) { 42 | return nil, someErr 43 | }) 44 | if err != someErr { 45 | t.Errorf("Do error = %v; want someErr %v", err, someErr) 46 | } 47 | if v != nil { 48 | t.Errorf("unexpected non-nil value %#v", v) 49 | } 50 | } 51 | 52 | func TestDoDupSuppress(t *testing.T) { 53 | var g Group[string, any] 54 | var wg1, wg2 sync.WaitGroup 55 | c := make(chan string, 1) 56 | var calls int32 57 | fn := func() (interface{}, error) { 58 | if atomic.AddInt32(&calls, 1) == 1 { 59 | // First invocation. 60 | wg1.Done() 61 | } 62 | v := <-c 63 | c <- v // pump; make available for any future calls 64 | 65 | time.Sleep(10 * time.Millisecond) // let more goroutines enter Do 66 | 67 | return v, nil 68 | } 69 | 70 | const n = 10 71 | wg1.Add(1) 72 | for i := 0; i < n; i++ { 73 | wg1.Add(1) 74 | wg2.Add(1) 75 | go func() { 76 | defer wg2.Done() 77 | wg1.Done() 78 | v, err, _ := g.Do("key", fn) 79 | if err != nil { 80 | t.Errorf("Do error: %v", err) 81 | return 82 | } 83 | if s, _ := v.(string); s != "bar" { 84 | t.Errorf("Do = %T %v; want %q", v, v, "bar") 85 | } 86 | }() 87 | } 88 | wg1.Wait() 89 | // At least one goroutine is in fn now and all of them have at 90 | // least reached the line before the Do. 91 | c <- "bar" 92 | wg2.Wait() 93 | if got := atomic.LoadInt32(&calls); got <= 0 || got >= n { 94 | t.Errorf("number of calls = %d; want over 0 and less than %d", got, n) 95 | } 96 | } 97 | 98 | // Test that singleflight behaves correctly after Forget called. 99 | // See https://github.com/golang/go/issues/31420 100 | func TestForget(t *testing.T) { 101 | var g Group[string, any] 102 | 103 | var ( 104 | firstStarted = make(chan struct{}) 105 | unblockFirst = make(chan struct{}) 106 | firstFinished = make(chan struct{}) 107 | ) 108 | 109 | go func() { 110 | g.Do("key", func() (i interface{}, e error) { 111 | close(firstStarted) 112 | <-unblockFirst 113 | close(firstFinished) 114 | return 115 | }) 116 | }() 117 | <-firstStarted 118 | g.Forget("key") 119 | 120 | unblockSecond := make(chan struct{}) 121 | secondResult := g.DoChan("key", func() (i interface{}, e error) { 122 | <-unblockSecond 123 | return 2, nil 124 | }) 125 | 126 | close(unblockFirst) 127 | <-firstFinished 128 | 129 | thirdResult := g.DoChan("key", func() (i interface{}, e error) { 130 | return 3, nil 131 | }) 132 | 133 | close(unblockSecond) 134 | <-secondResult 135 | r := <-thirdResult 136 | if r.Val != 2 { 137 | t.Errorf("We should receive result produced by second call, expected: 2, got %d", r.Val) 138 | } 139 | } 140 | 141 | func TestDoChan(t *testing.T) { 142 | var g Group[string, any] 143 | ch := g.DoChan("key", func() (interface{}, error) { 144 | return "bar", nil 145 | }) 146 | 147 | res := <-ch 148 | v := res.Val 149 | err := res.Err 150 | if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { 151 | t.Errorf("Do = %v; want %v", got, want) 152 | } 153 | if err != nil { 154 | t.Errorf("Do error = %v", err) 155 | } 156 | } 157 | 158 | // Test singleflight behaves correctly after Do panic. 159 | // See https://github.com/golang/go/issues/41133 160 | func TestPanicDo(t *testing.T) { 161 | var g Group[string, any] 162 | fn := func() (interface{}, error) { 163 | panic("invalid memory address or nil pointer dereference") 164 | } 165 | 166 | const n = 5 167 | waited := int32(n) 168 | panicCount := int32(0) 169 | done := make(chan struct{}) 170 | for i := 0; i < n; i++ { 171 | go func() { 172 | defer func() { 173 | if err := recover(); err != nil { 174 | t.Logf("Got panic: %v\n%s", err, debug.Stack()) 175 | atomic.AddInt32(&panicCount, 1) 176 | } 177 | 178 | if atomic.AddInt32(&waited, -1) == 0 { 179 | close(done) 180 | } 181 | }() 182 | 183 | g.Do("key", fn) 184 | }() 185 | } 186 | 187 | select { 188 | case <-done: 189 | if panicCount != n { 190 | t.Errorf("Expect %d panic, but got %d", n, panicCount) 191 | } 192 | case <-time.After(time.Second): 193 | t.Fatalf("Do hangs") 194 | } 195 | } 196 | 197 | func TestGoexitDo(t *testing.T) { 198 | var g Group[string, any] 199 | fn := func() (interface{}, error) { 200 | runtime.Goexit() 201 | return nil, nil 202 | } 203 | 204 | const n = 5 205 | waited := int32(n) 206 | done := make(chan struct{}) 207 | for i := 0; i < n; i++ { 208 | go func() { 209 | var err error 210 | defer func() { 211 | if err != nil { 212 | t.Errorf("Error should be nil, but got: %v", err) 213 | } 214 | if atomic.AddInt32(&waited, -1) == 0 { 215 | close(done) 216 | } 217 | }() 218 | _, err, _ = g.Do("key", fn) 219 | }() 220 | } 221 | 222 | select { 223 | case <-done: 224 | case <-time.After(time.Second): 225 | t.Fatalf("Do hangs") 226 | } 227 | } 228 | 229 | func TestPanicDoChan(t *testing.T) { 230 | if runtime.GOOS == "js" { 231 | t.Skipf("js does not support exec") 232 | } 233 | 234 | if os.Getenv("TEST_PANIC_DOCHAN") != "" { 235 | defer func() { 236 | recover() 237 | }() 238 | 239 | g := new(Group[string, any]) 240 | ch := g.DoChan("", func() (interface{}, error) { 241 | panic("Panicking in DoChan") 242 | }) 243 | <-ch 244 | t.Fatalf("DoChan unexpectedly returned") 245 | } 246 | 247 | t.Parallel() 248 | 249 | cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v") 250 | cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1") 251 | out := new(bytes.Buffer) 252 | cmd.Stdout = out 253 | cmd.Stderr = out 254 | if err := cmd.Start(); err != nil { 255 | t.Fatal(err) 256 | } 257 | 258 | err := cmd.Wait() 259 | t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out) 260 | if err == nil { 261 | t.Errorf("Test subprocess passed; want a crash due to panic in DoChan") 262 | } 263 | if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) { 264 | t.Errorf("Test subprocess failed with an unexpected failure mode.") 265 | } 266 | if !bytes.Contains(out.Bytes(), []byte("Panicking in DoChan")) { 267 | t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in DoChan") 268 | } 269 | } 270 | 271 | func TestPanicDoSharedByDoChan(t *testing.T) { 272 | if runtime.GOOS == "js" { 273 | t.Skipf("js does not support exec") 274 | } 275 | 276 | if os.Getenv("TEST_PANIC_DOCHAN") != "" { 277 | blocked := make(chan struct{}) 278 | unblock := make(chan struct{}) 279 | 280 | g := new(Group[string, any]) 281 | go func() { 282 | defer func() { 283 | recover() 284 | }() 285 | g.Do("", func() (interface{}, error) { 286 | close(blocked) 287 | <-unblock 288 | panic("Panicking in Do") 289 | }) 290 | }() 291 | 292 | <-blocked 293 | ch := g.DoChan("", func() (interface{}, error) { 294 | panic("DoChan unexpectedly executed callback") 295 | }) 296 | close(unblock) 297 | <-ch 298 | t.Fatalf("DoChan unexpectedly returned") 299 | } 300 | 301 | t.Parallel() 302 | 303 | cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v") 304 | cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1") 305 | out := new(bytes.Buffer) 306 | cmd.Stdout = out 307 | cmd.Stderr = out 308 | if err := cmd.Start(); err != nil { 309 | t.Fatal(err) 310 | } 311 | 312 | err := cmd.Wait() 313 | t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out) 314 | if err == nil { 315 | t.Errorf("Test subprocess passed; want a crash due to panic in Do shared by DoChan") 316 | } 317 | if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) { 318 | t.Errorf("Test subprocess failed with an unexpected failure mode.") 319 | } 320 | if !bytes.Contains(out.Bytes(), []byte("Panicking in Do")) { 321 | t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in Do") 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /backend/packages/util/yaml/utils.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v3" 6 | "os" 7 | "path" 8 | ) 9 | 10 | func ParseYaml[T any](ps ...string) (*T, error) { 11 | obj := new(T) 12 | success := false 13 | 14 | var all []string 15 | for _, p := range ps { 16 | all = append(all, p) 17 | ext := path.Ext(p) 18 | all = append(all, p[:len(p)-len(ext)]+".dev"+ext) 19 | all = append(all, p[:len(p)-len(ext)]+".prod"+ext) 20 | } 21 | 22 | for _, p := range all { 23 | if err := parse(obj, p); err == nil { 24 | fmt.Println("Load Config from ", p) 25 | success = true 26 | } 27 | } 28 | if !success { 29 | return obj, fmt.Errorf("fail to load config from all these path :%v", all) 30 | } 31 | return obj, nil 32 | } 33 | 34 | func parse[T any](t *T, filePath string) (err error) { 35 | var f *os.File 36 | f, err = os.Open(filePath) 37 | if err != nil { 38 | return 39 | } 40 | defer func(f *os.File) { 41 | err = f.Close() 42 | }(f) 43 | 44 | if err = yaml.NewDecoder(f).Decode(t); err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /backend/web/assets/Inter-italic.var-d1401419.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/backend/web/assets/Inter-italic.var-d1401419.woff2 -------------------------------------------------------------------------------- /backend/web/assets/Inter-roman.var-17fe38ab.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/backend/web/assets/Inter-roman.var-17fe38ab.woff2 -------------------------------------------------------------------------------- /backend/web/assets/primeicons-131bc3bf.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/backend/web/assets/primeicons-131bc3bf.ttf -------------------------------------------------------------------------------- /backend/web/assets/primeicons-3824be50.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/backend/web/assets/primeicons-3824be50.woff2 -------------------------------------------------------------------------------- /backend/web/assets/primeicons-90a58d3a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/backend/web/assets/primeicons-90a58d3a.woff -------------------------------------------------------------------------------- /backend/web/assets/primeicons-ce852338.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/backend/web/assets/primeicons-ce852338.eot -------------------------------------------------------------------------------- /backend/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Food Truck 8 | 14 | 15 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /backend/web/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/images/cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/doc/images/cli.png -------------------------------------------------------------------------------- /doc/images/hexagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/doc/images/hexagonal.png -------------------------------------------------------------------------------- /doc/images/home-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/doc/images/home-page.png -------------------------------------------------------------------------------- /doc/images/pop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/doc/images/pop.png -------------------------------------------------------------------------------- /doc/images/your-location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/doc/images/your-location.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | backend: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - "8080:8080" 9 | depends_on: 10 | - redis 11 | environment: 12 | REDIS_HOST: redis 13 | redis: 14 | image: redis:latest 15 | ports: 16 | - "6379:6379" -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_REACT_APP_API_HOST='http://localhost:8080' -------------------------------------------------------------------------------- /frontend/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dist/assets/Inter-italic.var-d1401419.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/frontend/dist/assets/Inter-italic.var-d1401419.woff2 -------------------------------------------------------------------------------- /frontend/dist/assets/Inter-roman.var-17fe38ab.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/frontend/dist/assets/Inter-roman.var-17fe38ab.woff2 -------------------------------------------------------------------------------- /frontend/dist/assets/primeicons-131bc3bf.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/frontend/dist/assets/primeicons-131bc3bf.ttf -------------------------------------------------------------------------------- /frontend/dist/assets/primeicons-3824be50.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/frontend/dist/assets/primeicons-3824be50.woff2 -------------------------------------------------------------------------------- /frontend/dist/assets/primeicons-90a58d3a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/frontend/dist/assets/primeicons-90a58d3a.woff -------------------------------------------------------------------------------- /frontend/dist/assets/primeicons-ce852338.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormCms/food-truck/65cfc45661597c67c3ed7ed826d5b71fd0eb1ac7/frontend/dist/assets/primeicons-ce852338.eot -------------------------------------------------------------------------------- /frontend/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Food Truck 8 | 14 | 15 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/dist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Food Truck 8 | 14 | 15 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-typescript-basic", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.6.8", 13 | "http-proxy-middleware": "^3.0.0", 14 | "primeflex": "^3.3.1", 15 | "primeicons": "^6.0.1", 16 | "primereact": "^10.5.1", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-leaflet": "^4.2.1", 20 | "swr": "^2.2.5" 21 | }, 22 | "devDependencies": { 23 | "@types/leaflet": "^1.9.12", 24 | "@types/react": "latest", 25 | "@types/react-dom": "latest", 26 | "@vitejs/plugin-react": "latest", 27 | "typescript": "latest", 28 | "vite": "latest" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | axios: 9 | specifier: ^1.6.8 10 | version: 1.6.8 11 | http-proxy-middleware: 12 | specifier: ^3.0.0 13 | version: 3.0.0 14 | primeflex: 15 | specifier: ^3.3.1 16 | version: 3.3.1 17 | primeicons: 18 | specifier: ^6.0.1 19 | version: 6.0.1 20 | primereact: 21 | specifier: ^10.5.1 22 | version: 10.6.5(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0) 23 | react: 24 | specifier: ^18.2.0 25 | version: 18.2.0 26 | react-dom: 27 | specifier: ^18.2.0 28 | version: 18.2.0(react@18.2.0) 29 | react-leaflet: 30 | specifier: ^4.2.1 31 | version: 4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0) 32 | swr: 33 | specifier: ^2.2.5 34 | version: 2.2.5(react@18.2.0) 35 | 36 | devDependencies: 37 | '@types/leaflet': 38 | specifier: ^1.9.12 39 | version: 1.9.12 40 | '@types/react': 41 | specifier: latest 42 | version: 18.2.31 43 | '@types/react-dom': 44 | specifier: latest 45 | version: 18.2.14 46 | '@vitejs/plugin-react': 47 | specifier: latest 48 | version: 4.1.0(vite@4.5.0) 49 | typescript: 50 | specifier: latest 51 | version: 5.2.2 52 | vite: 53 | specifier: latest 54 | version: 4.5.0 55 | 56 | packages: 57 | 58 | /@ampproject/remapping@2.2.1: 59 | resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} 60 | engines: {node: '>=6.0.0'} 61 | dependencies: 62 | '@jridgewell/gen-mapping': 0.3.3 63 | '@jridgewell/trace-mapping': 0.3.20 64 | dev: true 65 | 66 | /@babel/code-frame@7.22.13: 67 | resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} 68 | engines: {node: '>=6.9.0'} 69 | dependencies: 70 | '@babel/highlight': 7.22.20 71 | chalk: 2.4.2 72 | dev: true 73 | 74 | /@babel/compat-data@7.23.2: 75 | resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} 76 | engines: {node: '>=6.9.0'} 77 | dev: true 78 | 79 | /@babel/core@7.23.2: 80 | resolution: {integrity: sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==} 81 | engines: {node: '>=6.9.0'} 82 | dependencies: 83 | '@ampproject/remapping': 2.2.1 84 | '@babel/code-frame': 7.22.13 85 | '@babel/generator': 7.23.0 86 | '@babel/helper-compilation-targets': 7.22.15 87 | '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.2) 88 | '@babel/helpers': 7.23.2 89 | '@babel/parser': 7.23.0 90 | '@babel/template': 7.22.15 91 | '@babel/traverse': 7.23.2 92 | '@babel/types': 7.23.0 93 | convert-source-map: 2.0.0 94 | debug: 4.3.4 95 | gensync: 1.0.0-beta.2 96 | json5: 2.2.3 97 | semver: 6.3.1 98 | transitivePeerDependencies: 99 | - supports-color 100 | dev: true 101 | 102 | /@babel/generator@7.23.0: 103 | resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} 104 | engines: {node: '>=6.9.0'} 105 | dependencies: 106 | '@babel/types': 7.23.0 107 | '@jridgewell/gen-mapping': 0.3.3 108 | '@jridgewell/trace-mapping': 0.3.20 109 | jsesc: 2.5.2 110 | dev: true 111 | 112 | /@babel/helper-compilation-targets@7.22.15: 113 | resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} 114 | engines: {node: '>=6.9.0'} 115 | dependencies: 116 | '@babel/compat-data': 7.23.2 117 | '@babel/helper-validator-option': 7.22.15 118 | browserslist: 4.22.1 119 | lru-cache: 5.1.1 120 | semver: 6.3.1 121 | dev: true 122 | 123 | /@babel/helper-environment-visitor@7.22.20: 124 | resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} 125 | engines: {node: '>=6.9.0'} 126 | dev: true 127 | 128 | /@babel/helper-function-name@7.23.0: 129 | resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} 130 | engines: {node: '>=6.9.0'} 131 | dependencies: 132 | '@babel/template': 7.22.15 133 | '@babel/types': 7.23.0 134 | dev: true 135 | 136 | /@babel/helper-hoist-variables@7.22.5: 137 | resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} 138 | engines: {node: '>=6.9.0'} 139 | dependencies: 140 | '@babel/types': 7.23.0 141 | dev: true 142 | 143 | /@babel/helper-module-imports@7.22.15: 144 | resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} 145 | engines: {node: '>=6.9.0'} 146 | dependencies: 147 | '@babel/types': 7.23.0 148 | dev: true 149 | 150 | /@babel/helper-module-transforms@7.23.0(@babel/core@7.23.2): 151 | resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} 152 | engines: {node: '>=6.9.0'} 153 | peerDependencies: 154 | '@babel/core': ^7.0.0 155 | dependencies: 156 | '@babel/core': 7.23.2 157 | '@babel/helper-environment-visitor': 7.22.20 158 | '@babel/helper-module-imports': 7.22.15 159 | '@babel/helper-simple-access': 7.22.5 160 | '@babel/helper-split-export-declaration': 7.22.6 161 | '@babel/helper-validator-identifier': 7.22.20 162 | dev: true 163 | 164 | /@babel/helper-plugin-utils@7.22.5: 165 | resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} 166 | engines: {node: '>=6.9.0'} 167 | dev: true 168 | 169 | /@babel/helper-simple-access@7.22.5: 170 | resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} 171 | engines: {node: '>=6.9.0'} 172 | dependencies: 173 | '@babel/types': 7.23.0 174 | dev: true 175 | 176 | /@babel/helper-split-export-declaration@7.22.6: 177 | resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} 178 | engines: {node: '>=6.9.0'} 179 | dependencies: 180 | '@babel/types': 7.23.0 181 | dev: true 182 | 183 | /@babel/helper-string-parser@7.22.5: 184 | resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} 185 | engines: {node: '>=6.9.0'} 186 | dev: true 187 | 188 | /@babel/helper-validator-identifier@7.22.20: 189 | resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} 190 | engines: {node: '>=6.9.0'} 191 | dev: true 192 | 193 | /@babel/helper-validator-option@7.22.15: 194 | resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} 195 | engines: {node: '>=6.9.0'} 196 | dev: true 197 | 198 | /@babel/helpers@7.23.2: 199 | resolution: {integrity: sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==} 200 | engines: {node: '>=6.9.0'} 201 | dependencies: 202 | '@babel/template': 7.22.15 203 | '@babel/traverse': 7.23.2 204 | '@babel/types': 7.23.0 205 | transitivePeerDependencies: 206 | - supports-color 207 | dev: true 208 | 209 | /@babel/highlight@7.22.20: 210 | resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} 211 | engines: {node: '>=6.9.0'} 212 | dependencies: 213 | '@babel/helper-validator-identifier': 7.22.20 214 | chalk: 2.4.2 215 | js-tokens: 4.0.0 216 | dev: true 217 | 218 | /@babel/parser@7.23.0: 219 | resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} 220 | engines: {node: '>=6.0.0'} 221 | hasBin: true 222 | dependencies: 223 | '@babel/types': 7.23.0 224 | dev: true 225 | 226 | /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.23.2): 227 | resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==} 228 | engines: {node: '>=6.9.0'} 229 | peerDependencies: 230 | '@babel/core': ^7.0.0-0 231 | dependencies: 232 | '@babel/core': 7.23.2 233 | '@babel/helper-plugin-utils': 7.22.5 234 | dev: true 235 | 236 | /@babel/plugin-transform-react-jsx-source@7.22.5(@babel/core@7.23.2): 237 | resolution: {integrity: sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==} 238 | engines: {node: '>=6.9.0'} 239 | peerDependencies: 240 | '@babel/core': ^7.0.0-0 241 | dependencies: 242 | '@babel/core': 7.23.2 243 | '@babel/helper-plugin-utils': 7.22.5 244 | dev: true 245 | 246 | /@babel/runtime@7.23.2: 247 | resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} 248 | engines: {node: '>=6.9.0'} 249 | dependencies: 250 | regenerator-runtime: 0.14.0 251 | dev: false 252 | 253 | /@babel/template@7.22.15: 254 | resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} 255 | engines: {node: '>=6.9.0'} 256 | dependencies: 257 | '@babel/code-frame': 7.22.13 258 | '@babel/parser': 7.23.0 259 | '@babel/types': 7.23.0 260 | dev: true 261 | 262 | /@babel/traverse@7.23.2: 263 | resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} 264 | engines: {node: '>=6.9.0'} 265 | dependencies: 266 | '@babel/code-frame': 7.22.13 267 | '@babel/generator': 7.23.0 268 | '@babel/helper-environment-visitor': 7.22.20 269 | '@babel/helper-function-name': 7.23.0 270 | '@babel/helper-hoist-variables': 7.22.5 271 | '@babel/helper-split-export-declaration': 7.22.6 272 | '@babel/parser': 7.23.0 273 | '@babel/types': 7.23.0 274 | debug: 4.3.4 275 | globals: 11.12.0 276 | transitivePeerDependencies: 277 | - supports-color 278 | dev: true 279 | 280 | /@babel/types@7.23.0: 281 | resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} 282 | engines: {node: '>=6.9.0'} 283 | dependencies: 284 | '@babel/helper-string-parser': 7.22.5 285 | '@babel/helper-validator-identifier': 7.22.20 286 | to-fast-properties: 2.0.0 287 | dev: true 288 | 289 | /@esbuild/android-arm64@0.18.20: 290 | resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} 291 | engines: {node: '>=12'} 292 | cpu: [arm64] 293 | os: [android] 294 | requiresBuild: true 295 | dev: true 296 | optional: true 297 | 298 | /@esbuild/android-arm@0.18.20: 299 | resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} 300 | engines: {node: '>=12'} 301 | cpu: [arm] 302 | os: [android] 303 | requiresBuild: true 304 | dev: true 305 | optional: true 306 | 307 | /@esbuild/android-x64@0.18.20: 308 | resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} 309 | engines: {node: '>=12'} 310 | cpu: [x64] 311 | os: [android] 312 | requiresBuild: true 313 | dev: true 314 | optional: true 315 | 316 | /@esbuild/darwin-arm64@0.18.20: 317 | resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} 318 | engines: {node: '>=12'} 319 | cpu: [arm64] 320 | os: [darwin] 321 | requiresBuild: true 322 | dev: true 323 | optional: true 324 | 325 | /@esbuild/darwin-x64@0.18.20: 326 | resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} 327 | engines: {node: '>=12'} 328 | cpu: [x64] 329 | os: [darwin] 330 | requiresBuild: true 331 | dev: true 332 | optional: true 333 | 334 | /@esbuild/freebsd-arm64@0.18.20: 335 | resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} 336 | engines: {node: '>=12'} 337 | cpu: [arm64] 338 | os: [freebsd] 339 | requiresBuild: true 340 | dev: true 341 | optional: true 342 | 343 | /@esbuild/freebsd-x64@0.18.20: 344 | resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} 345 | engines: {node: '>=12'} 346 | cpu: [x64] 347 | os: [freebsd] 348 | requiresBuild: true 349 | dev: true 350 | optional: true 351 | 352 | /@esbuild/linux-arm64@0.18.20: 353 | resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} 354 | engines: {node: '>=12'} 355 | cpu: [arm64] 356 | os: [linux] 357 | requiresBuild: true 358 | dev: true 359 | optional: true 360 | 361 | /@esbuild/linux-arm@0.18.20: 362 | resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} 363 | engines: {node: '>=12'} 364 | cpu: [arm] 365 | os: [linux] 366 | requiresBuild: true 367 | dev: true 368 | optional: true 369 | 370 | /@esbuild/linux-ia32@0.18.20: 371 | resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} 372 | engines: {node: '>=12'} 373 | cpu: [ia32] 374 | os: [linux] 375 | requiresBuild: true 376 | dev: true 377 | optional: true 378 | 379 | /@esbuild/linux-loong64@0.18.20: 380 | resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} 381 | engines: {node: '>=12'} 382 | cpu: [loong64] 383 | os: [linux] 384 | requiresBuild: true 385 | dev: true 386 | optional: true 387 | 388 | /@esbuild/linux-mips64el@0.18.20: 389 | resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} 390 | engines: {node: '>=12'} 391 | cpu: [mips64el] 392 | os: [linux] 393 | requiresBuild: true 394 | dev: true 395 | optional: true 396 | 397 | /@esbuild/linux-ppc64@0.18.20: 398 | resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} 399 | engines: {node: '>=12'} 400 | cpu: [ppc64] 401 | os: [linux] 402 | requiresBuild: true 403 | dev: true 404 | optional: true 405 | 406 | /@esbuild/linux-riscv64@0.18.20: 407 | resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} 408 | engines: {node: '>=12'} 409 | cpu: [riscv64] 410 | os: [linux] 411 | requiresBuild: true 412 | dev: true 413 | optional: true 414 | 415 | /@esbuild/linux-s390x@0.18.20: 416 | resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} 417 | engines: {node: '>=12'} 418 | cpu: [s390x] 419 | os: [linux] 420 | requiresBuild: true 421 | dev: true 422 | optional: true 423 | 424 | /@esbuild/linux-x64@0.18.20: 425 | resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} 426 | engines: {node: '>=12'} 427 | cpu: [x64] 428 | os: [linux] 429 | requiresBuild: true 430 | dev: true 431 | optional: true 432 | 433 | /@esbuild/netbsd-x64@0.18.20: 434 | resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} 435 | engines: {node: '>=12'} 436 | cpu: [x64] 437 | os: [netbsd] 438 | requiresBuild: true 439 | dev: true 440 | optional: true 441 | 442 | /@esbuild/openbsd-x64@0.18.20: 443 | resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} 444 | engines: {node: '>=12'} 445 | cpu: [x64] 446 | os: [openbsd] 447 | requiresBuild: true 448 | dev: true 449 | optional: true 450 | 451 | /@esbuild/sunos-x64@0.18.20: 452 | resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} 453 | engines: {node: '>=12'} 454 | cpu: [x64] 455 | os: [sunos] 456 | requiresBuild: true 457 | dev: true 458 | optional: true 459 | 460 | /@esbuild/win32-arm64@0.18.20: 461 | resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} 462 | engines: {node: '>=12'} 463 | cpu: [arm64] 464 | os: [win32] 465 | requiresBuild: true 466 | dev: true 467 | optional: true 468 | 469 | /@esbuild/win32-ia32@0.18.20: 470 | resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} 471 | engines: {node: '>=12'} 472 | cpu: [ia32] 473 | os: [win32] 474 | requiresBuild: true 475 | dev: true 476 | optional: true 477 | 478 | /@esbuild/win32-x64@0.18.20: 479 | resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} 480 | engines: {node: '>=12'} 481 | cpu: [x64] 482 | os: [win32] 483 | requiresBuild: true 484 | dev: true 485 | optional: true 486 | 487 | /@jridgewell/gen-mapping@0.3.3: 488 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 489 | engines: {node: '>=6.0.0'} 490 | dependencies: 491 | '@jridgewell/set-array': 1.1.2 492 | '@jridgewell/sourcemap-codec': 1.4.15 493 | '@jridgewell/trace-mapping': 0.3.20 494 | dev: true 495 | 496 | /@jridgewell/resolve-uri@3.1.1: 497 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 498 | engines: {node: '>=6.0.0'} 499 | dev: true 500 | 501 | /@jridgewell/set-array@1.1.2: 502 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 503 | engines: {node: '>=6.0.0'} 504 | dev: true 505 | 506 | /@jridgewell/sourcemap-codec@1.4.15: 507 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 508 | dev: true 509 | 510 | /@jridgewell/trace-mapping@0.3.20: 511 | resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} 512 | dependencies: 513 | '@jridgewell/resolve-uri': 3.1.1 514 | '@jridgewell/sourcemap-codec': 1.4.15 515 | dev: true 516 | 517 | /@react-leaflet/core@2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0): 518 | resolution: {integrity: sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==} 519 | peerDependencies: 520 | leaflet: ^1.9.0 521 | react: ^18.0.0 522 | react-dom: ^18.0.0 523 | dependencies: 524 | leaflet: 1.9.4 525 | react: 18.2.0 526 | react-dom: 18.2.0(react@18.2.0) 527 | dev: false 528 | 529 | /@types/babel__core@7.20.3: 530 | resolution: {integrity: sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==} 531 | dependencies: 532 | '@babel/parser': 7.23.0 533 | '@babel/types': 7.23.0 534 | '@types/babel__generator': 7.6.6 535 | '@types/babel__template': 7.4.3 536 | '@types/babel__traverse': 7.20.3 537 | dev: true 538 | 539 | /@types/babel__generator@7.6.6: 540 | resolution: {integrity: sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==} 541 | dependencies: 542 | '@babel/types': 7.23.0 543 | dev: true 544 | 545 | /@types/babel__template@7.4.3: 546 | resolution: {integrity: sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==} 547 | dependencies: 548 | '@babel/parser': 7.23.0 549 | '@babel/types': 7.23.0 550 | dev: true 551 | 552 | /@types/babel__traverse@7.20.3: 553 | resolution: {integrity: sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==} 554 | dependencies: 555 | '@babel/types': 7.23.0 556 | dev: true 557 | 558 | /@types/geojson@7946.0.14: 559 | resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} 560 | dev: true 561 | 562 | /@types/http-proxy@1.17.14: 563 | resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==} 564 | dependencies: 565 | '@types/node': 20.12.12 566 | dev: false 567 | 568 | /@types/leaflet@1.9.12: 569 | resolution: {integrity: sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==} 570 | dependencies: 571 | '@types/geojson': 7946.0.14 572 | dev: true 573 | 574 | /@types/node@20.12.12: 575 | resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} 576 | dependencies: 577 | undici-types: 5.26.5 578 | dev: false 579 | 580 | /@types/prop-types@15.7.9: 581 | resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==} 582 | 583 | /@types/react-dom@18.2.14: 584 | resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==} 585 | dependencies: 586 | '@types/react': 18.2.31 587 | dev: true 588 | 589 | /@types/react-transition-group@4.4.8: 590 | resolution: {integrity: sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg==} 591 | dependencies: 592 | '@types/react': 18.3.2 593 | dev: false 594 | 595 | /@types/react@18.2.31: 596 | resolution: {integrity: sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==} 597 | dependencies: 598 | '@types/prop-types': 15.7.9 599 | '@types/scheduler': 0.16.5 600 | csstype: 3.1.2 601 | 602 | /@types/react@18.3.2: 603 | resolution: {integrity: sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==} 604 | dependencies: 605 | '@types/prop-types': 15.7.9 606 | csstype: 3.1.2 607 | dev: false 608 | 609 | /@types/scheduler@0.16.5: 610 | resolution: {integrity: sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==} 611 | 612 | /@vitejs/plugin-react@4.1.0(vite@4.5.0): 613 | resolution: {integrity: sha512-rM0SqazU9iqPUraQ2JlIvReeaxOoRj6n+PzB1C0cBzIbd8qP336nC39/R9yPi3wVcah7E7j/kdU1uCUqMEU4OQ==} 614 | engines: {node: ^14.18.0 || >=16.0.0} 615 | peerDependencies: 616 | vite: ^4.2.0 617 | dependencies: 618 | '@babel/core': 7.23.2 619 | '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.23.2) 620 | '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.23.2) 621 | '@types/babel__core': 7.20.3 622 | react-refresh: 0.14.0 623 | vite: 4.5.0 624 | transitivePeerDependencies: 625 | - supports-color 626 | dev: true 627 | 628 | /ansi-styles@3.2.1: 629 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 630 | engines: {node: '>=4'} 631 | dependencies: 632 | color-convert: 1.9.3 633 | dev: true 634 | 635 | /asynckit@0.4.0: 636 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 637 | dev: false 638 | 639 | /axios@1.6.8: 640 | resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} 641 | dependencies: 642 | follow-redirects: 1.15.6(debug@4.3.4) 643 | form-data: 4.0.0 644 | proxy-from-env: 1.1.0 645 | transitivePeerDependencies: 646 | - debug 647 | dev: false 648 | 649 | /braces@3.0.2: 650 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 651 | engines: {node: '>=8'} 652 | dependencies: 653 | fill-range: 7.0.1 654 | dev: false 655 | 656 | /browserslist@4.22.1: 657 | resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} 658 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 659 | hasBin: true 660 | dependencies: 661 | caniuse-lite: 1.0.30001553 662 | electron-to-chromium: 1.4.565 663 | node-releases: 2.0.13 664 | update-browserslist-db: 1.0.13(browserslist@4.22.1) 665 | dev: true 666 | 667 | /caniuse-lite@1.0.30001553: 668 | resolution: {integrity: sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==} 669 | dev: true 670 | 671 | /chalk@2.4.2: 672 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 673 | engines: {node: '>=4'} 674 | dependencies: 675 | ansi-styles: 3.2.1 676 | escape-string-regexp: 1.0.5 677 | supports-color: 5.5.0 678 | dev: true 679 | 680 | /client-only@0.0.1: 681 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 682 | dev: false 683 | 684 | /color-convert@1.9.3: 685 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 686 | dependencies: 687 | color-name: 1.1.3 688 | dev: true 689 | 690 | /color-name@1.1.3: 691 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 692 | dev: true 693 | 694 | /combined-stream@1.0.8: 695 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 696 | engines: {node: '>= 0.8'} 697 | dependencies: 698 | delayed-stream: 1.0.0 699 | dev: false 700 | 701 | /convert-source-map@2.0.0: 702 | resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 703 | dev: true 704 | 705 | /csstype@3.1.2: 706 | resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 707 | 708 | /debug@4.3.4: 709 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 710 | engines: {node: '>=6.0'} 711 | peerDependencies: 712 | supports-color: '*' 713 | peerDependenciesMeta: 714 | supports-color: 715 | optional: true 716 | dependencies: 717 | ms: 2.1.2 718 | 719 | /delayed-stream@1.0.0: 720 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 721 | engines: {node: '>=0.4.0'} 722 | dev: false 723 | 724 | /dom-helpers@5.2.1: 725 | resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} 726 | dependencies: 727 | '@babel/runtime': 7.23.2 728 | csstype: 3.1.2 729 | dev: false 730 | 731 | /electron-to-chromium@1.4.565: 732 | resolution: {integrity: sha512-XbMoT6yIvg2xzcbs5hCADi0dXBh4//En3oFXmtPX+jiyyiCTiM9DGFT2SLottjpEs9Z8Mh8SqahbR96MaHfuSg==} 733 | dev: true 734 | 735 | /esbuild@0.18.20: 736 | resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} 737 | engines: {node: '>=12'} 738 | hasBin: true 739 | requiresBuild: true 740 | optionalDependencies: 741 | '@esbuild/android-arm': 0.18.20 742 | '@esbuild/android-arm64': 0.18.20 743 | '@esbuild/android-x64': 0.18.20 744 | '@esbuild/darwin-arm64': 0.18.20 745 | '@esbuild/darwin-x64': 0.18.20 746 | '@esbuild/freebsd-arm64': 0.18.20 747 | '@esbuild/freebsd-x64': 0.18.20 748 | '@esbuild/linux-arm': 0.18.20 749 | '@esbuild/linux-arm64': 0.18.20 750 | '@esbuild/linux-ia32': 0.18.20 751 | '@esbuild/linux-loong64': 0.18.20 752 | '@esbuild/linux-mips64el': 0.18.20 753 | '@esbuild/linux-ppc64': 0.18.20 754 | '@esbuild/linux-riscv64': 0.18.20 755 | '@esbuild/linux-s390x': 0.18.20 756 | '@esbuild/linux-x64': 0.18.20 757 | '@esbuild/netbsd-x64': 0.18.20 758 | '@esbuild/openbsd-x64': 0.18.20 759 | '@esbuild/sunos-x64': 0.18.20 760 | '@esbuild/win32-arm64': 0.18.20 761 | '@esbuild/win32-ia32': 0.18.20 762 | '@esbuild/win32-x64': 0.18.20 763 | dev: true 764 | 765 | /escalade@3.1.1: 766 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 767 | engines: {node: '>=6'} 768 | dev: true 769 | 770 | /escape-string-regexp@1.0.5: 771 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 772 | engines: {node: '>=0.8.0'} 773 | dev: true 774 | 775 | /eventemitter3@4.0.7: 776 | resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} 777 | dev: false 778 | 779 | /fill-range@7.0.1: 780 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 781 | engines: {node: '>=8'} 782 | dependencies: 783 | to-regex-range: 5.0.1 784 | dev: false 785 | 786 | /follow-redirects@1.15.6(debug@4.3.4): 787 | resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} 788 | engines: {node: '>=4.0'} 789 | peerDependencies: 790 | debug: '*' 791 | peerDependenciesMeta: 792 | debug: 793 | optional: true 794 | dependencies: 795 | debug: 4.3.4 796 | dev: false 797 | 798 | /form-data@4.0.0: 799 | resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} 800 | engines: {node: '>= 6'} 801 | dependencies: 802 | asynckit: 0.4.0 803 | combined-stream: 1.0.8 804 | mime-types: 2.1.35 805 | dev: false 806 | 807 | /fsevents@2.3.3: 808 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 809 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 810 | os: [darwin] 811 | requiresBuild: true 812 | dev: true 813 | optional: true 814 | 815 | /gensync@1.0.0-beta.2: 816 | resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 817 | engines: {node: '>=6.9.0'} 818 | dev: true 819 | 820 | /globals@11.12.0: 821 | resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 822 | engines: {node: '>=4'} 823 | dev: true 824 | 825 | /has-flag@3.0.0: 826 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 827 | engines: {node: '>=4'} 828 | dev: true 829 | 830 | /http-proxy-middleware@3.0.0: 831 | resolution: {integrity: sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==} 832 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 833 | dependencies: 834 | '@types/http-proxy': 1.17.14 835 | debug: 4.3.4 836 | http-proxy: 1.18.1(debug@4.3.4) 837 | is-glob: 4.0.3 838 | is-plain-obj: 3.0.0 839 | micromatch: 4.0.5 840 | transitivePeerDependencies: 841 | - supports-color 842 | dev: false 843 | 844 | /http-proxy@1.18.1(debug@4.3.4): 845 | resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} 846 | engines: {node: '>=8.0.0'} 847 | dependencies: 848 | eventemitter3: 4.0.7 849 | follow-redirects: 1.15.6(debug@4.3.4) 850 | requires-port: 1.0.0 851 | transitivePeerDependencies: 852 | - debug 853 | dev: false 854 | 855 | /is-extglob@2.1.1: 856 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 857 | engines: {node: '>=0.10.0'} 858 | dev: false 859 | 860 | /is-glob@4.0.3: 861 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 862 | engines: {node: '>=0.10.0'} 863 | dependencies: 864 | is-extglob: 2.1.1 865 | dev: false 866 | 867 | /is-number@7.0.0: 868 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 869 | engines: {node: '>=0.12.0'} 870 | dev: false 871 | 872 | /is-plain-obj@3.0.0: 873 | resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} 874 | engines: {node: '>=10'} 875 | dev: false 876 | 877 | /js-tokens@4.0.0: 878 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 879 | 880 | /jsesc@2.5.2: 881 | resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} 882 | engines: {node: '>=4'} 883 | hasBin: true 884 | dev: true 885 | 886 | /json5@2.2.3: 887 | resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 888 | engines: {node: '>=6'} 889 | hasBin: true 890 | dev: true 891 | 892 | /leaflet@1.9.4: 893 | resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} 894 | dev: false 895 | 896 | /loose-envify@1.4.0: 897 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 898 | hasBin: true 899 | dependencies: 900 | js-tokens: 4.0.0 901 | dev: false 902 | 903 | /lru-cache@5.1.1: 904 | resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 905 | dependencies: 906 | yallist: 3.1.1 907 | dev: true 908 | 909 | /micromatch@4.0.5: 910 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 911 | engines: {node: '>=8.6'} 912 | dependencies: 913 | braces: 3.0.2 914 | picomatch: 2.3.1 915 | dev: false 916 | 917 | /mime-db@1.52.0: 918 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 919 | engines: {node: '>= 0.6'} 920 | dev: false 921 | 922 | /mime-types@2.1.35: 923 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 924 | engines: {node: '>= 0.6'} 925 | dependencies: 926 | mime-db: 1.52.0 927 | dev: false 928 | 929 | /ms@2.1.2: 930 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 931 | 932 | /nanoid@3.3.6: 933 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 934 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 935 | hasBin: true 936 | dev: true 937 | 938 | /node-releases@2.0.13: 939 | resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} 940 | dev: true 941 | 942 | /object-assign@4.1.1: 943 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 944 | engines: {node: '>=0.10.0'} 945 | dev: false 946 | 947 | /picocolors@1.0.0: 948 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 949 | dev: true 950 | 951 | /picomatch@2.3.1: 952 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 953 | engines: {node: '>=8.6'} 954 | dev: false 955 | 956 | /postcss@8.4.31: 957 | resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 958 | engines: {node: ^10 || ^12 || >=14} 959 | dependencies: 960 | nanoid: 3.3.6 961 | picocolors: 1.0.0 962 | source-map-js: 1.0.2 963 | dev: true 964 | 965 | /primeflex@3.3.1: 966 | resolution: {integrity: sha512-zaOq3YvcOYytbAmKv3zYc+0VNS9Wg5d37dfxZnveKBFPr7vEIwfV5ydrpiouTft8MVW6qNjfkaQphHSnvgQbpQ==} 967 | dev: false 968 | 969 | /primeicons@6.0.1: 970 | resolution: {integrity: sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==} 971 | dev: false 972 | 973 | /primereact@10.6.5(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0): 974 | resolution: {integrity: sha512-YbdvyIRIGSfZVhtIbltizUISBuXVXBn2S4/491xdDJLrxohjjf0uIogu+/sSKRBVVT17b6zFEwARTvc9yUA7tQ==} 975 | engines: {node: '>=14.0.0'} 976 | peerDependencies: 977 | '@types/react': ^17.0.0 || ^18.0.0 978 | react: ^17.0.0 || ^18.0.0 979 | react-dom: ^17.0.0 || ^18.0.0 980 | peerDependenciesMeta: 981 | '@types/react': 982 | optional: true 983 | dependencies: 984 | '@types/react': 18.2.31 985 | '@types/react-transition-group': 4.4.8 986 | react: 18.2.0 987 | react-dom: 18.2.0(react@18.2.0) 988 | react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) 989 | dev: false 990 | 991 | /prop-types@15.8.1: 992 | resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 993 | dependencies: 994 | loose-envify: 1.4.0 995 | object-assign: 4.1.1 996 | react-is: 16.13.1 997 | dev: false 998 | 999 | /proxy-from-env@1.1.0: 1000 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 1001 | dev: false 1002 | 1003 | /react-dom@18.2.0(react@18.2.0): 1004 | resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} 1005 | peerDependencies: 1006 | react: ^18.2.0 1007 | dependencies: 1008 | loose-envify: 1.4.0 1009 | react: 18.2.0 1010 | scheduler: 0.23.0 1011 | dev: false 1012 | 1013 | /react-is@16.13.1: 1014 | resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 1015 | dev: false 1016 | 1017 | /react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0): 1018 | resolution: {integrity: sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==} 1019 | peerDependencies: 1020 | leaflet: ^1.9.0 1021 | react: ^18.0.0 1022 | react-dom: ^18.0.0 1023 | dependencies: 1024 | '@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0) 1025 | leaflet: 1.9.4 1026 | react: 18.2.0 1027 | react-dom: 18.2.0(react@18.2.0) 1028 | dev: false 1029 | 1030 | /react-refresh@0.14.0: 1031 | resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} 1032 | engines: {node: '>=0.10.0'} 1033 | dev: true 1034 | 1035 | /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): 1036 | resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} 1037 | peerDependencies: 1038 | react: '>=16.6.0' 1039 | react-dom: '>=16.6.0' 1040 | dependencies: 1041 | '@babel/runtime': 7.23.2 1042 | dom-helpers: 5.2.1 1043 | loose-envify: 1.4.0 1044 | prop-types: 15.8.1 1045 | react: 18.2.0 1046 | react-dom: 18.2.0(react@18.2.0) 1047 | dev: false 1048 | 1049 | /react@18.2.0: 1050 | resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 1051 | engines: {node: '>=0.10.0'} 1052 | dependencies: 1053 | loose-envify: 1.4.0 1054 | dev: false 1055 | 1056 | /regenerator-runtime@0.14.0: 1057 | resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} 1058 | dev: false 1059 | 1060 | /requires-port@1.0.0: 1061 | resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} 1062 | dev: false 1063 | 1064 | /rollup@3.29.4: 1065 | resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} 1066 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 1067 | hasBin: true 1068 | optionalDependencies: 1069 | fsevents: 2.3.3 1070 | dev: true 1071 | 1072 | /scheduler@0.23.0: 1073 | resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} 1074 | dependencies: 1075 | loose-envify: 1.4.0 1076 | dev: false 1077 | 1078 | /semver@6.3.1: 1079 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1080 | hasBin: true 1081 | dev: true 1082 | 1083 | /source-map-js@1.0.2: 1084 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 1085 | engines: {node: '>=0.10.0'} 1086 | dev: true 1087 | 1088 | /supports-color@5.5.0: 1089 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 1090 | engines: {node: '>=4'} 1091 | dependencies: 1092 | has-flag: 3.0.0 1093 | dev: true 1094 | 1095 | /swr@2.2.5(react@18.2.0): 1096 | resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} 1097 | peerDependencies: 1098 | react: ^16.11.0 || ^17.0.0 || ^18.0.0 1099 | dependencies: 1100 | client-only: 0.0.1 1101 | react: 18.2.0 1102 | use-sync-external-store: 1.2.2(react@18.2.0) 1103 | dev: false 1104 | 1105 | /to-fast-properties@2.0.0: 1106 | resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 1107 | engines: {node: '>=4'} 1108 | dev: true 1109 | 1110 | /to-regex-range@5.0.1: 1111 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1112 | engines: {node: '>=8.0'} 1113 | dependencies: 1114 | is-number: 7.0.0 1115 | dev: false 1116 | 1117 | /typescript@5.2.2: 1118 | resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} 1119 | engines: {node: '>=14.17'} 1120 | hasBin: true 1121 | dev: true 1122 | 1123 | /undici-types@5.26.5: 1124 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 1125 | dev: false 1126 | 1127 | /update-browserslist-db@1.0.13(browserslist@4.22.1): 1128 | resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} 1129 | hasBin: true 1130 | peerDependencies: 1131 | browserslist: '>= 4.21.0' 1132 | dependencies: 1133 | browserslist: 4.22.1 1134 | escalade: 3.1.1 1135 | picocolors: 1.0.0 1136 | dev: true 1137 | 1138 | /use-sync-external-store@1.2.2(react@18.2.0): 1139 | resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} 1140 | peerDependencies: 1141 | react: ^16.8.0 || ^17.0.0 || ^18.0.0 1142 | dependencies: 1143 | react: 18.2.0 1144 | dev: false 1145 | 1146 | /vite@4.5.0: 1147 | resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} 1148 | engines: {node: ^14.18.0 || >=16.0.0} 1149 | hasBin: true 1150 | peerDependencies: 1151 | '@types/node': '>= 14' 1152 | less: '*' 1153 | lightningcss: ^1.21.0 1154 | sass: '*' 1155 | stylus: '*' 1156 | sugarss: '*' 1157 | terser: ^5.4.0 1158 | peerDependenciesMeta: 1159 | '@types/node': 1160 | optional: true 1161 | less: 1162 | optional: true 1163 | lightningcss: 1164 | optional: true 1165 | sass: 1166 | optional: true 1167 | stylus: 1168 | optional: true 1169 | sugarss: 1170 | optional: true 1171 | terser: 1172 | optional: true 1173 | dependencies: 1174 | esbuild: 0.18.20 1175 | postcss: 8.4.31 1176 | rollup: 3.29.4 1177 | optionalDependencies: 1178 | fsevents: 2.3.3 1179 | dev: true 1180 | 1181 | /yallist@3.1.1: 1182 | resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1183 | dev: true 1184 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | 43 | .leaflet-container { 44 | height: 100vh; 45 | width: 100vh; 46 | } -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import reactLogo from './assets/react.svg'; 3 | import { Button } from 'primereact/button'; 4 | import { InputText } from 'primereact/inputtext'; 5 | 6 | import 'primereact/resources/themes/lara-light-indigo/theme.css'; //theme 7 | import 'primereact/resources/primereact.min.css'; //core css 8 | import 'primeicons/primeicons.css'; //icons 9 | import 'primeflex/primeflex.css'; // flex 10 | import './App.css'; 11 | import Map from "./map/Map"; 12 | 13 | 14 | function App() { 15 | return ( 16 |
17 | 18 |
Version 1.0
19 |
20 | ); 21 | } 22 | export default App; -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/config.ts: -------------------------------------------------------------------------------- 1 | export const Config = { 2 | APIHost : import.meta.env.VITE_REACT_APP_API_HOST || '', 3 | } -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { PrimeReactProvider } from "primereact/api"; 4 | import App from './App' 5 | import './index.css' 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /frontend/src/map/FacilityMarkers.tsx: -------------------------------------------------------------------------------- 1 | import {Marker, Popup, useMap, useMapEvents} from "react-leaflet"; 2 | import {getDistanceFromLatLonInKm} from "../utils/getDistance"; 3 | import useSWR from "swr"; 4 | import {Config} from "../config"; 5 | import {fetcher} from "../utils/fetcher"; 6 | import {useState} from "react"; 7 | 8 | export function FacilityMarkers(){ 9 | const [location, setLocation] = useState({lat:0, lng:0, radius:0}) 10 | const getLocation = ()=> { 11 | const {lat, lng} = map.getCenter() 12 | const se = map.getBounds().getSouthEast() 13 | const radius = getDistanceFromLatLonInKm(lat, lng, se.lat, se.lng) 14 | setLocation({ lat,lng,radius}) 15 | } 16 | const map = useMapEvents({ 17 | zoomend:(e) => { 18 | getLocation() 19 | }, 20 | moveend:(e) => { 21 | getLocation() 22 | } 23 | }) 24 | const {data : facilities} = useSWR(Config.APIHost + `/api/facilities?lat=${location.lat}&lon=${location.lng}&radius=${location.radius}`, fetcher) 25 | return facilities &&<> 26 | {facilities.map(item => { 27 | return 28 | 29 | {item.applicant}
{item.locationDescription}
{item.foodItems} 30 |
31 |
32 | })} 33 | 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/map/Map.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import {MapContainer, Marker, Popup, TileLayer, useMap} from 'react-leaflet' 3 | import useSWR from 'swr' 4 | import {fetcher} from "../utils/fetcher"; 5 | import {Config} from "../config"; 6 | import {FacilityMarkers} from "./FacilityMarkers"; 7 | import {Button} from "primereact/button"; 8 | import {SwitchLocation} from "./SwitchLocation"; 9 | 10 | export default function Map() { 11 | const {data: center} = useSWR(Config.APIHost + '/api/facilities/center', fetcher) 12 | const [your, setYour] = useState(false) 13 | 14 | return ( 15 |
16 | {!your &&
); 27 | } -------------------------------------------------------------------------------- /frontend/src/map/SwitchLocation.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | import {Marker, Popup, useMap} from "react-leaflet"; 3 | 4 | export function SwitchLocation(){ 5 | const [position, setPosition] = useState(null); 6 | const map = useMap(); 7 | const [loading, setLoading] = useState(false) 8 | 9 | useEffect(() => { 10 | map.locate({setView: true, maxZoom: 16}).on('locationfound', function (e: L.LocationEvent) { 11 | setLoading(true) 12 | setPosition(e.latlng); 13 | map.flyTo(e.latlng, map.getZoom()); 14 | setLoading(false) 15 | }); 16 | }, [map]); 17 | 18 | return <> 19 | {loading&&
Loading
} 20 | {position === null ? null : ( 21 | 22 | You are here 23 | ) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /frontend/src/models/facility.ts: -------------------------------------------------------------------------------- 1 | interface Facility { 2 | locationID: string; 3 | applicant: string; 4 | facilityType: string; 5 | cnn: string; 6 | locationDescription: string; 7 | address: string; 8 | blockLot: string; 9 | block: string; 10 | lot: string; 11 | permit: string; 12 | status: string; 13 | foodItems: string; 14 | x: number; 15 | y: number; 16 | latitude: number; 17 | longitude: number; 18 | schedule: string; 19 | daysHours: string; 20 | NOISent: string; 21 | approved: string; 22 | received: string; 23 | priorPermit: string; 24 | expirationDate: string; 25 | location: string; 26 | firePreventionDistricts: string; 27 | policeDistricts: string; 28 | supervisorDistricts: string; 29 | zipCodes: string; 30 | neighborhoodsOld: string; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/utils/fetcher.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | export const fetcher = (...args: any[]) => fetch(...args).then(res => res.json()) 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/utils/getDistance.ts: -------------------------------------------------------------------------------- 1 | export function getDistanceFromLatLonInKm(lat1: number, lon1: number, lat2: number, lon2: number): number { 2 | const R: number = 6371; // Radius of the Earth in kilometers 3 | const dLat: number = deg2rad(lat2 - lat1); // Convert latitude difference to radians 4 | const dLon: number = deg2rad(lon2 - lon1); // Convert longitude difference to radians 5 | const a: number = 6 | Math.sin(dLat / 2) * Math.sin(dLat / 2) + 7 | Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * 8 | Math.sin(dLon / 2) * Math.sin(dLon / 2); 9 | const c: number = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 10 | const distance: number = R * c; // Distance in kilometers 11 | return distance; 12 | } 13 | function deg2rad(deg: number): number { 14 | return deg * (Math.PI / 180); 15 | } -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/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 | }) 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Food trucks 2 | This is a demo project showcase usage of Go, React, Redis, Docker. 3 | Data is from San Francisco's food truck open dataset. 4 | 5 | ## Features 6 | As a user, when you go to the websites' home page, you can see a list of marker, 7 | each marker represents a food truck. 8 | 9 | ![img.png](doc/images/home-page.png) 10 | 11 | When you click a marker, you can see the food truck's applicant, location, and food items. 12 | 13 | ![img.png](doc/images/pop.png) 14 | 15 | If you live in San Francisco, you want see the trucks near your location, 16 | you can click 'Your Location' button, the map will be switched to your location 17 | 18 | ![img.png](doc/images/your-location.png) 19 | 20 | As a admin, you can search all trucks who are serving a type of food, e.g. taco 21 | 22 | ![img.png](doc/images/cli.png) 23 | 24 | ## Tech Stacks 25 | ### Backend 26 | - Redis as in memory database 27 | - redis str, get truck(marshalled as json) by ID 28 | - redis geo, to search nearby trucks by latitude, longitude, and radius. 29 | - redis zset, to search a list of trucks by food items it served. 30 | - Go 31 | - Iris Web Framework 32 | 33 | ### Frontend 34 | - React 35 | - react-leaflet for map related feature 36 | - swr for state management 37 | 38 | ## Design pattern and best practice 39 | - *hexagonal architecture* 40 | 41 | ![img_1.png](doc/images/hexagonal.png) 42 | 43 | The core of backend is /backend/packages/services/facilitySvc.go, service layer doesn't depend on 44 | storage layer, and doesn't depend on UI layer. 45 | Both Cli and Web can use facility service. 46 | 47 | - *Dependency Injection* 48 | 49 | facilitySvc is not hardcoded depending on rdb package(my own Redis lib), so if I want 50 | change storage to mysql or mongodb later, I can implement the interface 51 | and inject the implementation to service. 52 | 53 | This also conform to Open/Close principle, the facilitySvc is open to extend functionality, 54 | but close to code change 55 | 56 | - *Separation of Concern* 57 | 58 | When do frontend coding, I also tried to apply this principle, for the truck map frontend. 59 | I use 3 components to render map (Map, FacilityMaker, SwitchLocation), each component care about it's own job, improved readability. 60 | 61 | - *Modular and DRY - Don't repeat your self* 62 | 63 | I aimed to separate business logic from infrastructure code in our project. Taking the `facilitySvc` as an example: 64 | each facility can have multiple food items, and each food item can be associated with multiple facilities. 65 | This relationship pertains to business logic. In contrast, connecting to Redis and marshalling objects to JSON strings are common infrastructure tasks. 66 | 67 | By wrapping Redis operations into a standalone package, instead of embedding this code within the facility service, 68 | I made the codebase more modular and reusable. This separation improves maintainability and allows infrastructure code 69 | to be reused across different services without duplication. 70 | 71 | - *Template pattern* 72 | 73 | There are a lot of boilerplate to start a web application, 74 | I put these code to /backend/packages/util/irisbase applying Template Pattern, 75 | so the main file(/backend/cmds/web/main) looks clean and straightforward. 76 | 77 | - *Error handling* 78 | 79 | Each function annotate error detail (e.g. which line throws the error, the cause of the error). 80 | In develop mode, the API returns error detail to help frontend user to locate the issue. 81 | In production mode, the API just return an 500 error to hide technical detail 82 | 83 | - *Generic Programming to improve ability to reuse code* 84 | 85 | For example, the parse function in /backend/packages/util/yaml.go demonstrates how to create a reusable utility for parsing 86 | YAML files into any specified type. By using generics, we can create a single, versatile function that works with 87 | any data structure, enhancing our ability to write clean, reusable, and maintainable code. 88 | ``` 89 | func parse[T any](t *T, filePath string) (err error) { 90 | var f *os.File 91 | f, err = os.Open(filePath) 92 | if err != nil { 93 | return 94 | } 95 | defer func(f *os.File) { 96 | err = f.Close() 97 | }(f) 98 | 99 | if err = yaml.NewDecoder(f).Decode(t); err != nil { 100 | return err 101 | } 102 | return nil 103 | } 104 | ``` 105 | 106 | 107 | ## Implementations 108 | ### Frontend 109 | #### Code Structure 110 | ``` 111 | --frontend/ 112 | ----src/ 113 | ------map/ 114 | --------FacilityMarkers.tsx # add markers to map 115 | --------Map.tsx # map container 116 | --------SwitchLoaction.tsx # switch to your location 117 | ------models/ 118 | ------utils/ 119 | ------config.ts # global configs 120 | ----.env.development # development enviroment virables 121 | ``` 122 | #### Api call and state management 123 | All meaningful code resides in /frontend/src/map, code in models and utils is very simple. 124 | the useSWR hook combine api call and state management, one single line of code save the trouble of useEffect hook. 125 | ``` 126 | const {data: center} = useSWR(Config.APIHost + '/api/facilities/center', fetcher) 127 | ``` 128 | #### Environment variables 129 | The frontend app might run in two mode 130 | ##### Development Mode 131 | In development mode (pnpm dev), I start two web server, 132 | http://localhost:8080 as backend http://localhost:5173 as frontend, so the api endpoint is http://localhost:8080/api/***. 133 | I put this dev environment settings to .env.development 134 | ``` 135 | VITE_REACT_APP_API_HOST='http://localhost:8080' 136 | ``` 137 | ##### Production Mode 138 | In production mode frontend and backend are served as single app(the distribution of frontend is copied to 139 | /backend/web, and served by backend web server). 140 | I can use relative path to call backend api /api/facilities. In production there won't be .env file, 141 | so the api host default to empty string''. 142 | 143 | ##### config.js 144 | All environment reading code are put into config.ts , ensure single source of truth. 145 | ``` 146 | export const Config = { 147 | APIHost : import.meta.env.VITE_REACT_APP_API_HOST || '', 148 | } 149 | ``` 150 | #### Map Related Features 151 | I tried google map API first, but it's not totally free, I don't want checkin API Key to repo. And I want 152 | people can easily play with this app, so I followed this link https://medium.com/@ujjwaltiwari2/a-guide-to-using-openstreetmap-with-react-70932389b8b1 153 | to use react-leaflet 154 | ### Backend 155 | #### Code structure 156 | ``` 157 | --cmds 158 | ----cli/ # entrance of cli 159 | ----web/ # entrance of web 160 | --packages 161 | ----controllers/ # endpoint of APIs 162 | ----models/ 163 | ----services/ # implement business logic of food trucks 164 | ----utils/ # infrastrcutres 165 | --web/ # put frontend distribution here 166 | ``` 167 | #### Seed Data 168 | In packages/services/facilitySvc Seed() function, it read configs/data.csv, and parse it as Facility array, 169 | then populate the data to redis. 170 | 171 | ### Endpoints 172 | - */api/facilities/center* Get the center of all trucks 173 | - */api/facilities?lat=&lon=&radius=* Get the facilities near the center with in the radius 174 | ### Cli 175 | - share Facility Service with web, provides function of search facility by food items 176 | 177 | ## Installation 178 | If you don't have go, node, pnpm installed on you local machine, you can simply use docker compose to start the app. 179 | ### Docker 180 | in the root directory of the project, run 181 | ```shell 182 | docker-compose up 183 | ``` 184 | When you see messages similar to below, then the app is up. 185 | ```shell 186 | food-truck-backend-1 | Now listening on: 187 | food-truck-backend-1 | > Network: http://172.21.0.3:8080 188 | food-truck-backend-1 | > Local: http://localhost:8080 189 | food-truck-backend-1 | Application started. Press CTRL+C to shut down. 190 | ``` 191 | ### Web 192 | Use your browser, go to http://localhost:8080 to see the food truck app. 193 | ### Cli 194 | to run cli 195 | ```shell 196 | # use docker ps to check docker container name 197 | ⚡➜ ~ docker ps 198 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 199 | 0f6039ec90d6 food-truck-backend "./main" 5 minutes ago Up 5 minutes 0.0.0.0:8080->8080/tcp food-truck-backend-1 200 | 40f20e7696e4 redis:latest "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 0.0.0.0:6379->6379/tcp food-truck-redis-1 201 | 202 | # start a shell session 203 | ⚡➜ ~ docker exec -it food-truck-backend-1 /bin/bash 204 | 205 | # in the shell session, run ./food-cli 206 | root@0f6039ec90d6:/go/src/app# ./food-cli 207 | Load Config from ./configs/cli.yaml 208 | 209 | # when you saw the 'Enter Food Item to search facility:', input a food item you want to find 210 | Enter Food Item to search facility: breakfast 211 | Munch A Bunch MISSION ST: 14TH ST to 15TH ST (1800 - 1899) 212 | Munch A Bunch BRYANT ST: ALAMEDA ST intersection 213 | Munch A Bunch FULTON ST: FRANKLIN ST to GOUGH ST (300 - 399) 214 | Munch A Bunch LARKIN ST: FERN ST to BUSH ST (1127 - 1199) 215 | Munch A Bunch 12TH ST: ISIS ST to BERNICE ST (332 - 365) 216 | Munch A Bunch 07TH ST: CLEVELAND ST to HARRISON ST (314 - 399) 217 | Munch A Bunch PARNASSUS AVE: HILLWAY AVE to 03RD AVE (400 - 599) 218 | Munch A Bunch 17TH ST: SAN BRUNO AVE to UTAH ST (2200 - 2299) 219 | ``` 220 | 221 | ## Development 222 | ### Spin up a redis server 223 | ```shell 224 | docker run --name food-redis -d -p 6379:6379 redis 225 | ``` 226 | ### Start backend 227 | go to /backend, 228 | ```shell 229 | go run backend/cmds/web/main.go 230 | ``` 231 | if you got the following error, it means backend can not connect to a host 'redis' 232 | ``` 233 | panic: facilitySvc.go:58 failed to cache facilities, dial tcp: lookup redis: no such host 234 | ``` 235 | you can add 'redis' to you development machine's /etc/hosts file 236 | ``` 237 | 127.0.0.1 localhost redis 238 | ``` 239 | or you can modify /backend/configs/web.yaml file, change the following line 240 | ``` 241 | addr: redis:6379 242 | ``` 243 | to 244 | ``` 245 | addr: localhost:6379 246 | ``` 247 | ### Start CLI 248 | ``` 249 | go run backend/cmds/cli/main.go 250 | ``` 251 | Cli's config file is at /backend/configs/cli.yaml 252 | 253 | ### Start Frontend 254 | you need to install node, pnpm, then go to frontend/, run 255 | ``` 256 | pnpm install 257 | pnpm dev 258 | ``` -------------------------------------------------------------------------------- /scripts/build_front.sh: -------------------------------------------------------------------------------- 1 | pushd ../frontend 2 | pnpm build 3 | rm -rf ../backend/web 4 | cp -a dist ../backend/web 5 | -------------------------------------------------------------------------------- /scripts/start_redis.sh: -------------------------------------------------------------------------------- 1 | docker run --name food-redis -d -p 6379:6379 redis --------------------------------------------------------------------------------