diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..8cbc915 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,56 @@ +name: Go Tests + +on: [ pull_request ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22.6' + + - name: Start PostgreSQL + run: | + docker rm -f gecko-postgres-test > /dev/null 2>&1 || true + + docker run -d \ + --name gecko-postgres-test \ + -p 8081:5432 \ + -e POSTGRES_PASSWORD=your_strong_password \ + -e POSTGRES_USER=postgres \ + postgres:10.4 > /dev/null + + # Wait for PostgreSQL to be ready + for i in {1..30}; do + if docker exec gecko-postgres-test pg_isready -U postgres -h localhost; then + break + fi + sleep 1 + done + + # Create database and setup schema + - name: Setup Database + run: | + # Create the test database + docker exec gecko-postgres-test psql -U postgres -c "CREATE DATABASE testdb;" + + # Create the table + docker exec gecko-postgres-test psql -U postgres -d testdb -c " + CREATE TABLE IF NOT EXISTS documents ( + name VARCHAR(255) PRIMARY KEY, + content JSONB + );" + env: + PGPASSWORD: your_strong_password + + - name: Start Application + run: | + make + ./bin/gecko -db "postgresql://postgres:your_strong_password@localhost:8081/testdb?sslmode=disable" -port 8080 & + go test -v ./... diff --git a/Dockerfile b/Dockerfile index ee1de55..c7f1cf5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,11 +9,8 @@ ENV PATH="/go/bin:${PATH}" WORKDIR $GOPATH/src/github.com/ACED-IDP/gecko/ - - COPY go.mod . COPY go.sum . - RUN go mod download COPY . . diff --git a/Makefile b/Makefile index 6466343..41dfcaf 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,6 @@ _default: bin/gecko bin/gecko: gecko/*.go # help: run the server go build -o bin/gecko + +clean: + rm -f bin/gecko \ No newline at end of file diff --git a/README.md b/README.md index 0bf06d7..d6dbd21 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,6 @@ # gecko -gecko is a configuration server used for fetching inserting user specified configurations that are set during etl jobs and read from the frontend. - -## helm cluster setup - -Helm setup should be much simpler since env vars are defined. - -``` -./init_cluster_pg.sh -go build -o bin/gecko && ./bin/gecko -go test -v ./... -``` +gecko is a configuration server used for fetching inserting user specified configurations that are set during etl jobs or frontend actions. ## local setup @@ -19,6 +9,10 @@ Make sure the below command matches whatever was specified in the init db script ``` ./init_postgres.sh go build -o bin/gecko -./bin/gecko -db "postgresql://postgres:your_strong_password@localhost:5432/testdb?sslmode=disable" +./bin/gecko -db "postgresql://postgres:your_strong_password@localhost:5432/testdb?sslmode=disable" -port 8080 go test -v ./... ``` + +## helm cluster setup + +See helm charts for cluster setup. diff --git a/gecko/config/explorerConfig.go b/gecko/config/explorerConfig.go index 40cb63d..7ec5b7a 100644 --- a/gecko/config/explorerConfig.go +++ b/gecko/config/explorerConfig.go @@ -1,17 +1,17 @@ package config type FieldConfig struct { - Field string `json:"field"` - DataField string `json:"dataField"` - Index string `json:"index"` + Field string `json:"field,omitempty"` + DataField string `json:"dataField,omitempty"` + Index string `json:"index,omitempty"` Label string `json:"label"` - Type string `json:"type"` + Type string `json:"type,omitempty"` } type FilterTab struct { - Title string `json:"title"` + Title string `json:"title,omitempty"` Fields []string `json:"fields"` - FieldsConfig map[string]FieldConfig `json:"fieldsConfig"` + FieldsConfig map[string]FieldConfig `json:"fieldsConfig,omitempty"` } type FiltersConfig struct { @@ -19,9 +19,10 @@ type FiltersConfig struct { } type TableConfig struct { - Enabled bool `json:"enabled"` - Fields []string `json:"fields"` - Columns map[string]TableColumnsConfig `json:"columns"` + Enabled bool `json:"enabled"` + Fields []string `json:"fields"` + Columns map[string]TableColumnsConfig `json:"columns,omitempty"` + DetailsConfig TableDetailsConfig `json:"detailsConfig,omitempty"` } type TableColumnsConfig struct { @@ -29,10 +30,35 @@ type TableColumnsConfig struct { Title string `json:"title"` } +type TableDetailsConfig struct { + Panel string `json:"panel,omitempty"` + Mode string `json:"mode,omitempty"` + IDField string `json:"idField,omitempty"` + FilterField string `json:"filterField,omitempty"` + Title string `json:"title,omitempty"` + NodeType string `json:"nodeType,omitempty"` + NodeFields map[string]string `json:"nodeFields,omitempty"` +} + type GuppyConfig struct { - DataType string `json:"dataType"` - NodeCountTitle string `json:"nodeCountTitle"` - FieldMapping []string `json:"fieldMapping"` + DataType string `json:"dataType"` + NodeCountTitle string `json:"nodeCountTitle"` + FieldMapping []GuppyFieldMapping `json:"fieldMapping,omitempty"` + AccessibleFieldCheckList []string `json:"accessibleFieldCheckList,omitempty"` + AccessibleValidationField string `json:"accessibleValidationField,omitempty"` + ManifestMapping ManifestMapping `json:"manifestMapping,omitempty"` +} + +type GuppyFieldMapping struct { + Field string `json:"field,omitempty"` + Name string `json:"name,omitempty"` +} + +type ManifestMapping struct { + ResourceIndexType string `json:"resourceIndexType,omitempty"` + ResourceIdField string `json:"resourceIdField,omitempty"` + ReferenceIdFieldInResourceIndex string `json:"referenceIdFieldInResourceIndex,omitempty"` + ReferenceIdFieldInDataIndex string `json:"referenceIdFieldInDataIndex,omitempty"` } type Chart struct { @@ -40,13 +66,32 @@ type Chart struct { Title string `json:"title"` } +type ButtonConfig struct { + Enabled bool `json:"enabled,omitempty"` + Type string `json:"type,omitempty"` + Action string `json:"action,omitempty"` + Title string `json:"title,omitempty"` + LeftIcon string `json:"leftIcon,omitempty"` + RightIcon string `json:"rightIcon,omitempty"` + FileName string `json:"fileName,omitempty"` + ActionArgs ButtonActionArgs `json:"actionArgs,omitempty"` +} + +type ButtonActionArgs struct { + ResourceIndexType string `json:"resourceIndexType,omitempty"` + ResourceIdField string `json:"resourceIdField,omitempty"` + ReferenceIdFieldInDataIndex string `json:"referenceIdFieldInDataIndex,omitempty"` + ReferenceIdFieldInResourceIndex string `json:"referenceIdFieldInResourceIndex,omitempty"` + FileFields []string `json:"fileFields,omitempty"` +} + type ConfigItem struct { TabTitle string `json:"tabTitle"` GuppyConfig GuppyConfig `json:"guppyConfig"` - Charts map[string]Chart `json:"charts"` - Filters FiltersConfig `json:"filters"` // Updated type - Table TableConfig `json:"table"` // Updated type - Dropdowns map[string]any `json:"dropdowns"` - Buttons []any `json:"buttons"` - LoginForDownload bool `json:"loginForDownload"` + Charts map[string]Chart `json:"charts,omitempty"` + Filters FiltersConfig `json:"filters"` + Table TableConfig `json:"table"` + Dropdowns map[string]any `json:"dropdowns,omitempty"` + Buttons []ButtonConfig `json:"buttons,omitempty"` + LoginForDownload bool `json:"loginForDownload,omitempty"` } diff --git a/gecko/server.go b/gecko/server.go index cc3a03f..70df6fb 100644 --- a/gecko/server.go +++ b/gecko/server.go @@ -58,20 +58,16 @@ func (server *Server) Init() (*Server, error) { if server.logger == nil { return nil, errors.New("gecko server initialized without logger") } - server.logger.logger.Printf("DB: %#v, JWTApp: %#v, Logger: %#v", server.db, server.jwtApp, server.logger) + server.logger.Info("DB: %#v, JWTApp: %#v, Logger: %#v", server.db, server.jwtApp, server.logger) return server, nil } func (server *Server) MakeRouter() *iris.Application { router := iris.New() if router == nil { - log.Fatal("Failed to initialize router") + server.logger.Error("Failed to initialize router") } router.Use(recoveryMiddleware) - router.Get("/", func(ctx iris.Context) { - server.logger.logger.Println("Root handler called") - ctx.JSON(iris.Map{"message": "Hello, World!"}) - }) router.OnErrorCode(iris.StatusNotFound, handleNotFound) router.Get("/health", server.handleHealth) router.Get("/config/{configId}", server.handleConfigGET) @@ -82,7 +78,7 @@ func (server *Server) MakeRouter() *iris.Application { router.UseRouter(func(ctx iris.Context) { req := ctx.Request() if req == nil || req.URL == nil { - log.Println("WARNING: Request or URL is nil") + server.logger.Warning("Request or URL is nil") ctx.StatusCode(http.StatusInternalServerError) ctx.WriteString("Internal Server Error") return @@ -90,9 +86,10 @@ func (server *Server) MakeRouter() *iris.Application { req.URL.Path = strings.TrimSuffix(req.URL.Path, "/") ctx.Next() }) + // Build the router to ensure it's ready for net/http if err := router.Build(); err != nil { - log.Fatalf("Failed to build Iris router: %v", err) + server.logger.Error("Failed to build Iris router: %v", err) } return router } @@ -125,7 +122,7 @@ func (server *Server) handleConfigGET(ctx iris.Context) { _ = errResponse.write(ctx) return } - server.logger.logger.Println(doc) + server.logger.Info("%#v", doc) _ = jsonResponseFrom(doc, http.StatusOK).write(ctx) } @@ -148,7 +145,7 @@ func (server *Server) handleConfigDELETE(ctx iris.Context) { } okmsg := map[string]any{"code": 200, "message": fmt.Sprintf("DELETED: %s", configId)} - server.logger.logger.Println(okmsg) + server.logger.Info("%#v", okmsg) _ = jsonResponseFrom(okmsg, http.StatusOK).write(ctx) } @@ -157,23 +154,30 @@ func (server *Server) handleConfigPUT(ctx iris.Context) { data := []config.ConfigItem{} body, err := ctx.GetBody() if err != nil { - msg := fmt.Sprintf("client query failed: %s", err.Error()) + msg := fmt.Sprintf("GetBody() failed: %s", err.Error()) errResponse := newErrorResponse(msg, 500, nil) errResponse.log.write(server.logger) _ = errResponse.write(ctx) return - + } + if !json.Valid(body) { + msg := "Invalid JSON format" + errResponse := newErrorResponse(msg, 400, nil) + errResponse.log.write(server.logger) + _ = errResponse.write(ctx) + return } errResponse := unmarshal(body, &data) if errResponse != nil { - fmt.Printf("HELLO DATA: ERR: %#v\n", errResponse) + msg := fmt.Sprintf("body data unmarshal failed: %s", errResponse.err) + errResponse := newErrorResponse(msg, 400, nil) errResponse.log.write(server.logger) _ = errResponse.write(ctx) return } err = configPUT(server.db, configId, data) if err != nil { - msg := fmt.Sprintf("client query failed: %s", err.Error()) + msg := fmt.Sprintf("configPut failed: %s", err.Error()) errResponse := newErrorResponse(msg, 500, nil) errResponse.log.write(server.logger) _ = errResponse.write(ctx) @@ -181,20 +185,20 @@ func (server *Server) handleConfigPUT(ctx iris.Context) { } okmsg := map[string]any{"code": 200, "message": fmt.Sprintf("ACCEPTED: %s", configId)} - server.logger.logger.Println(okmsg) + server.logger.Info("%#v", okmsg) _ = jsonResponseFrom(okmsg, http.StatusOK).write(ctx) } func (server *Server) handleHealth(ctx iris.Context) { - server.logger.logger.Println("Entering handleHealth") + server.logger.Info("Entering handleHealth") err := server.db.Ping() if err != nil { - server.logger.logger.Printf("Database ping failed: %v", err) + server.logger.Error("Database ping failed: %v", err) response := newErrorResponse("database unavailable", 500, nil) _ = response.write(ctx) return } - server.logger.logger.Println("Health check passed") + server.logger.Info("Health check passed") _ = jsonResponseFrom("Healthy", http.StatusOK).write(ctx) } @@ -220,7 +224,6 @@ func unmarshal(body []byte, x any) *ErrorResponse { if len(body) == 0 { return newErrorResponse("empty request body", http.StatusBadRequest, nil) } - err := json.Unmarshal(body, x) if err != nil { structType := reflect.TypeOf(x) diff --git a/init_cluster_pg.sh b/init_cluster_pg.sh deleted file mode 100644 index 9879b22..0000000 --- a/init_cluster_pg.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -psql -d postgres <