Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,22 @@ jobs:

docker run -d \
--name gecko-qdrant-test \
-p 6333:6333 \
-p 6334:6334 \
-e QDRANT__SERVICE__API_KEY=your_qdrant_api_key \
qdrant/qdrant:latest > /dev/null

# Wait for Qdrant to be ready
echo "Waiting for Qdrant to be ready..."
for i in {1..30}; do
if curl --silent --fail http://localhost:6334/readyz > /dev/null; then
if curl --silent --fail http://localhost:6333/readyz > /dev/null; then
echo "Qdrant is ready!"
break
fi
if [ $i -eq 30 ]; then
echo "Timeout waiting for Qdrant"
exit 1
fi
sleep 1
done

Expand All @@ -102,11 +109,20 @@ jobs:
-qdrant-api-key "your_qdrant_api_key" \
-qdrant-host localhost \
-qdrant-port 6334 &

# Wait for application to be ready
echo "Waiting for application to be ready..."
for i in {1..30}; do
if curl --silent --fail http://localhost:8080/health > /dev/null; then
echo "Application is ready!"
break
fi
if [ $i -eq 30 ]; then
echo "Timeout waiting for application"
# Show logs to help debug
kill $(jobs -p) || true
exit 1
fi
sleep 1
done
go test -v ./...
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
_default: bin/gecko
@: # if we have a command this silences "nothing to be done"

bin/gecko: gecko/*.go # help: run the server
go build -o bin/gecko
bin/gecko: main.go gecko/*.go # help: run the server
go build -o bin/gecko .

clean:
rm -f bin/gecko
Expand Down
2 changes: 1 addition & 1 deletion gecko/adapter/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func ToQdrantFilter(filter *HeadFilter) *qdrant.Filter {
return nil
}

mustConditions := make([]*qdrant.Condition, len(filter.Must), len(filter.Must))
mustConditions := make([]*qdrant.Condition, len(filter.Must))

for i, cond := range filter.Must {
switch v := cond.Match.Value.(type) {
Expand Down
72 changes: 48 additions & 24 deletions gecko/adapter/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,62 @@ package adapter

import "github.com/qdrant/go-client/qdrant"

// convertQdrantPointsResponse transforms a Qdrant QueryResponse to simplify payloads.
func ConvertQdrantPointsResponse(resp []*qdrant.ScoredPoint) []map[string]any {
result := make([]map[string]any, len(resp))
for i, point := range resp {
simplifiedPoint := map[string]any{
"id": point.Id.GetUuid(),
"score": point.Score,
func convertVectors(v *qdrant.VectorsOutput) map[string]any {
if v == nil || v.VectorsOptions == nil {
return nil
}
vectors := make(map[string]any)
switch vo := v.VectorsOptions.(type) {
case *qdrant.VectorsOutput_Vector:
if vo.Vector != nil {
vectors["default"] = vo.Vector.Data
}

// Convert vectors if present
if point.Vectors != nil {
vectors := make(map[string]any)
if vectorsMap := point.Vectors.GetVectors(); vectorsMap != nil {
for name, vec := range vectorsMap.Vectors {
case *qdrant.VectorsOutput_Vectors:
if vo.Vectors != nil && vo.Vectors.Vectors != nil {
for name, vec := range vo.Vectors.Vectors {
if vec != nil {
vectors[name] = vec.Data
}
} else if vector := point.Vectors.GetVector(); vector != nil {
vectors["default"] = vector.Data
}
simplifiedPoint["vectors"] = vectors
}
}
return vectors
}

// Convert payload if present
if point.Payload != nil {
payload := make(map[string]any)
for key, value := range point.Payload {
payload[key] = convertQdrantValueToJSON(value)
}
simplifiedPoint["payload"] = payload
func convertPayload(p map[string]*qdrant.Value) map[string]any {
if p == nil {
return nil
}
payload := make(map[string]any)
for key, value := range p {
payload[key] = convertQdrantValueToJSON(value)
}
return payload
}

// ConvertQdrantPointsResponse transforms a Qdrant QueryResponse to simplify payloads.
func ConvertQdrantPointsResponse(resp []*qdrant.ScoredPoint) []map[string]any {
result := make([]map[string]any, len(resp))
for i, point := range resp {
result[i] = map[string]any{
"id": point.Id.GetUuid(),
"score": point.Score,
"vectors": convertVectors(point.Vectors),
"payload": convertPayload(point.Payload),
}
}
return result
}

result[i] = simplifiedPoint
// ConvertQdrantRetrievedPointsResponse transforms a Qdrant GetResponse to simplify payloads.
func ConvertQdrantRetrievedPointsResponse(resp []*qdrant.RetrievedPoint) []map[string]any {
result := make([]map[string]any, len(resp))
for i, point := range resp {
result[i] = map[string]any{
"id": point.Id.GetUuid(),
"vectors": convertVectors(point.Vectors),
"payload": convertPayload(point.Payload),
}
}
return result
}
Expand Down
208 changes: 208 additions & 0 deletions gecko/handleAppCard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package gecko

import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"

"github.com/calypr/gecko/gecko/config"
"github.com/kataras/iris/v12"
)

// handleAppCardGET godoc
// @Summary Get a specific AppCard by projectId (perms)
// @Description Retrieves a single AppCard from the apps_page configuration by its perms value (used as projectId).
// @Tags Config
// @Produce json
// @Param projectId path string true "Project ID (AppCard perms value, e.g., HTAN_INT-BForePC)"
// @Success 200 {object} config.AppCard "The requested AppCard"
// @Failure 404 {object} ErrorResponse "AppCard not found"
// @Failure 500 {object} ErrorResponse "Server error"
// @Router /config/apps_page/appcard/{projectId} [get]
func (server *Server) handleAppCardGET(ctx iris.Context) {
configType := "apps_page"
configId := "1" // Matches the ID used in helm chart bootstrap

projectId := ctx.Params().Get("projectId")
if projectId == "" {
errResponse := newErrorResponse("Missing projectId parameter", 400, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}

var currentCfg config.AppsConfig
err := configGETGeneric(server.db, configId, configType, &currentCfg)
if errors.Is(err, sql.ErrNoRows) {
// No config exists yet → no AppCards
msg := fmt.Sprintf("AppCard with projectId (perms) %s not found (no config exists)", projectId)
errResponse := newErrorResponse(msg, 404, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}
if err != nil {
msg := fmt.Sprintf("Failed to retrieve apps_page config: %s", err)
errResponse := newErrorResponse(msg, 500, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}

// Find the matching AppCard by Perms
for _, card := range currentCfg.AppCards {
if card.Perms == projectId {
jsonResponseFrom(card, http.StatusOK).write(ctx)
return
}
}

// Not found
msg := fmt.Sprintf("AppCard with projectId (perms) %s not found", projectId)
errResponse := newErrorResponse(msg, 404, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
}

// handleAppCardPOST godoc
// @Summary Add or update an AppCard
// @Description Adds a new AppCard to the apps_page configuration or updates an existing one if the perms matches. Assumes a fixed configId "default" for apps_page.
// @Tags Config
// @Accept json
// @Produce json
// @Param body body config.AppCard true "AppCard details"
// @Success 200 {object} map[string]interface{} "AppCard added or updated"
// @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 404 {object} ErrorResponse "Config not found (if required)"
// @Failure 500 {object} ErrorResponse "Server error"
// @Router /config/apps_page/appcard [post]
func (server *Server) handleAppCardPOST(ctx iris.Context) {
configType := "apps_page"
configId := "1" // Matches the ID used in helm chart bootstrap

var currentCfg config.AppsConfig
err := configGETGeneric(server.db, configId, configType, &currentCfg)
if errors.Is(err, sql.ErrNoRows) {
// Initialize empty if not found
currentCfg = config.AppsConfig{AppCards: []config.AppCard{}}
} else if err != nil {
msg := fmt.Sprintf("Failed to get apps_page config: %s", err)
errResponse := newErrorResponse(msg, 500, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}

// Body already read in middleware; GetBody() returns cached version
body, _ := ctx.GetBody()
var newCard config.AppCard
if err := json.Unmarshal(body, &newCard); err != nil {
msg := "Invalid JSON format"
errResponse := newErrorResponse(msg, 400, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}

// Update if Perms exists, else append
updated := false
for i := range currentCfg.AppCards {
if currentCfg.AppCards[i].Perms == newCard.Perms {
currentCfg.AppCards[i] = newCard
updated = true
break
}
}
if !updated {
currentCfg.AppCards = append(currentCfg.AppCards, newCard)
}

// Save the updated config
err = configPUTGeneric(server.db, configId, configType, &currentCfg)
if err != nil {
msg := fmt.Sprintf("Failed to update apps_page config: %s", err)
errResponse := newErrorResponse(msg, 500, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}

jsonResponseFrom(
map[string]any{
"code": 200,
"message": fmt.Sprintf("AppCard with perms %s added or updated", newCard.Perms),
},
http.StatusOK,
).write(ctx)
}

// handleAppCardDELETE godoc
// @Summary Delete an AppCard
// @Description Deletes an AppCard from the apps_page configuration by projectId (perms). Assumes a fixed configId "default" for apps_page.
// @Tags Config
// @Produce json
// @Param projectId path string true "Project ID (AppCard perms value, e.g., HTAN_INT-BForePC)"
// @Success 200 {object} map[string]interface{} "AppCard deleted"
// @Failure 404 {object} ErrorResponse "AppCard or config not found"
// @Failure 500 {object} ErrorResponse "Server error"
// @Router /config/apps_page/appcard/{projectId} [delete]
func (server *Server) handleAppCardDELETE(ctx iris.Context) {
configType := "apps_page"
configId := "1" // Matches the ID used in helm chart bootstrap
projectId := ctx.Params().Get("projectId")

var currentCfg config.AppsConfig
err := configGETGeneric(server.db, configId, configType, &currentCfg)
if errors.Is(err, sql.ErrNoRows) {
msg := "No apps_page config found"
errResponse := newErrorResponse(msg, 404, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
} else if err != nil {
msg := fmt.Sprintf("Failed to get apps_page config: %s", err)
errResponse := newErrorResponse(msg, 500, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}

// Remove the matching AppCard by Perms
newCards := []config.AppCard{}
found := false
for _, card := range currentCfg.AppCards {
if card.Perms == projectId {
found = true
continue
}
newCards = append(newCards, card)
}
if !found {
msg := fmt.Sprintf("AppCard with projectId (perms) %s not found", projectId)
errResponse := newErrorResponse(msg, 404, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}
currentCfg.AppCards = newCards

// Save the updated config
err = configPUTGeneric(server.db, configId, configType, &currentCfg)
if err != nil {
msg := fmt.Sprintf("Failed to update apps_page config: %s", err)
errResponse := newErrorResponse(msg, 500, nil)
errResponse.log.write(server.Logger)
_ = errResponse.write(ctx)
return
}

jsonResponseFrom(
map[string]any{
"code": 200,
"message": fmt.Sprintf("AppCard with perms %s deleted", projectId),
},
http.StatusOK,
).write(ctx)
}
Loading