├── .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 && setYour(true)} label="Your Location" style={{ backgroundColor: 'var(--primary-color)', color: 'var(--primary-color-text)'}}/>}
17 | {center &&
18 |
22 |
23 | {your && }
24 |
25 | }
26 |
);
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 | 
10 |
11 | When you click a marker, you can see the food truck's applicant, location, and food items.
12 |
13 | 
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 | 
19 |
20 | As a admin, you can search all trucks who are serving a type of food, e.g. taco
21 |
22 | 
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 | 
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
--------------------------------------------------------------------------------