2024-05-02 21:45:32 +08:00
|
|
|
package gql_actions
|
|
|
|
|
|
|
|
import (
|
2024-08-30 05:40:52 +08:00
|
|
|
"bbb-graphql-middleware/config"
|
2024-08-09 03:50:41 +08:00
|
|
|
"bbb-graphql-middleware/internal/common"
|
2024-05-02 21:45:32 +08:00
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2024-08-08 22:28:46 +08:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2024-05-02 21:45:32 +08:00
|
|
|
log "github.com/sirupsen/logrus"
|
2024-05-22 03:11:17 +08:00
|
|
|
"io/ioutil"
|
2024-05-02 21:45:32 +08:00
|
|
|
"net/http"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
2024-07-01 02:34:22 +08:00
|
|
|
"time"
|
2024-05-02 21:45:32 +08:00
|
|
|
)
|
|
|
|
|
2024-08-30 05:40:52 +08:00
|
|
|
var graphqlActionsUrl = config.GetConfig().GraphqlActions.Url
|
2024-05-02 21:45:32 +08:00
|
|
|
|
|
|
|
func GraphqlActionsClient(
|
2024-07-06 00:35:08 +08:00
|
|
|
browserConnection *common.BrowserConnection) error {
|
2024-05-02 21:45:32 +08:00
|
|
|
|
|
|
|
log := log.WithField("_routine", "GraphqlActionsClient").WithField("browserConnectionId", browserConnection.Id)
|
|
|
|
log.Debug("Starting GraphqlActionsClient")
|
|
|
|
defer log.Debug("Finished GraphqlActionsClient")
|
|
|
|
|
|
|
|
RangeLoop:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-browserConnection.Context.Done():
|
|
|
|
break RangeLoop
|
|
|
|
case <-browserConnection.GraphqlActionsContext.Done():
|
|
|
|
log.Debug("GraphqlActionsContext cancelled!")
|
|
|
|
break RangeLoop
|
2024-07-06 00:35:08 +08:00
|
|
|
case fromBrowserMessage := <-browserConnection.FromBrowserToGqlActionsChannel.ReceiveChannel():
|
2024-05-02 21:45:32 +08:00
|
|
|
{
|
|
|
|
if fromBrowserMessage == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-06-25 21:27:44 +08:00
|
|
|
var browserMessage common.BrowserSubscribeMessage
|
|
|
|
err := json.Unmarshal(fromBrowserMessage, &browserMessage)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("failed to unmarshal message: %v", err)
|
|
|
|
continue
|
|
|
|
}
|
2024-05-02 21:45:32 +08:00
|
|
|
|
2024-06-25 21:27:44 +08:00
|
|
|
if browserMessage.Type == "subscribe" {
|
2024-05-22 03:11:17 +08:00
|
|
|
var errorMessage string
|
|
|
|
var mutationFuncName string
|
|
|
|
|
2024-06-25 21:27:44 +08:00
|
|
|
if strings.HasPrefix(browserMessage.Payload.Query, "mutation") {
|
|
|
|
if funcName, inputs, err := parseGraphQLMutation(browserMessage.Payload.Query, browserMessage.Payload.Variables); err == nil {
|
2024-05-22 03:11:17 +08:00
|
|
|
mutationFuncName = funcName
|
2024-07-01 02:34:22 +08:00
|
|
|
if err = SendGqlActionsRequest(funcName, inputs, browserConnection.BBBWebSessionVariables, log); err == nil {
|
2024-08-08 22:28:46 +08:00
|
|
|
//Add Prometheus Metrics
|
|
|
|
common.GqlMutationsCounter.With(prometheus.Labels{"operationName": browserMessage.Payload.OperationName}).Inc()
|
2024-05-02 21:45:32 +08:00
|
|
|
} else {
|
2024-05-22 03:11:17 +08:00
|
|
|
errorMessage = err.Error()
|
2024-05-02 21:45:32 +08:00
|
|
|
log.Error("It was not able to send the request to Graphql Actions", err)
|
|
|
|
}
|
|
|
|
} else {
|
2024-05-22 03:11:17 +08:00
|
|
|
errorMessage = "It was not able to parse graphQL query"
|
2024-05-02 21:45:32 +08:00
|
|
|
log.Error("It was not able to parse graphQL query", err)
|
|
|
|
}
|
|
|
|
}
|
2024-05-22 03:11:17 +08:00
|
|
|
|
|
|
|
if errorMessage != "" {
|
|
|
|
//Error on sending action, return error msg to client
|
|
|
|
browserResponseData := map[string]interface{}{
|
2024-06-25 21:27:44 +08:00
|
|
|
"id": browserMessage.ID,
|
2024-05-22 03:11:17 +08:00
|
|
|
"type": "error",
|
2024-05-30 04:43:17 +08:00
|
|
|
"payload": []interface{}{
|
|
|
|
map[string]interface{}{
|
|
|
|
"message": errorMessage,
|
2024-05-22 03:11:17 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2024-06-25 21:27:44 +08:00
|
|
|
jsonData, _ := json.Marshal(browserResponseData)
|
2024-07-06 00:35:08 +08:00
|
|
|
browserConnection.FromHasuraToBrowserChannel.Send(jsonData)
|
2024-05-22 03:11:17 +08:00
|
|
|
} else {
|
|
|
|
//Action sent successfully, return data msg to client
|
|
|
|
browserResponseData := map[string]interface{}{
|
2024-06-25 21:27:44 +08:00
|
|
|
"id": browserMessage.ID,
|
2024-05-23 02:51:12 +08:00
|
|
|
"type": "next",
|
2024-05-22 03:11:17 +08:00
|
|
|
"payload": map[string]interface{}{
|
|
|
|
"data": map[string]interface{}{
|
|
|
|
mutationFuncName: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2024-06-25 21:27:44 +08:00
|
|
|
jsonData, _ := json.Marshal(browserResponseData)
|
2024-07-06 00:35:08 +08:00
|
|
|
browserConnection.FromHasuraToBrowserChannel.Send(jsonData)
|
2024-05-22 03:11:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
//Return complete msg to client
|
|
|
|
browserResponseComplete := map[string]interface{}{
|
2024-06-25 21:27:44 +08:00
|
|
|
"id": browserMessage.ID,
|
2024-05-22 03:11:17 +08:00
|
|
|
"type": "complete",
|
|
|
|
}
|
2024-06-25 21:27:44 +08:00
|
|
|
jsonData, _ := json.Marshal(browserResponseComplete)
|
2024-07-06 00:35:08 +08:00
|
|
|
browserConnection.FromHasuraToBrowserChannel.Send(jsonData)
|
2024-05-02 21:45:32 +08:00
|
|
|
}
|
2024-05-22 03:11:17 +08:00
|
|
|
|
|
|
|
//Fallback to Hasura was disabled (keeping the code temporarily)
|
|
|
|
//fromBrowserToHasuraChannel.Send(fromBrowserMessage)
|
2024-05-02 21:45:32 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-01 02:34:22 +08:00
|
|
|
func SendGqlActionsRequest(funcName string, inputs map[string]interface{}, sessionVariables map[string]string, logger *log.Entry) error {
|
|
|
|
logger = logger.WithField("funcName", funcName).WithField("inputs", inputs)
|
|
|
|
|
2024-05-02 21:45:32 +08:00
|
|
|
data := GqlActionsRequestBody{
|
|
|
|
Action: GqlActionsAction{
|
|
|
|
Name: funcName,
|
|
|
|
},
|
|
|
|
Input: inputs,
|
|
|
|
SessionVariables: sessionVariables,
|
|
|
|
}
|
|
|
|
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if graphqlActionsUrl == "" {
|
|
|
|
return fmt.Errorf("No Graphql Actions Url (BBB_GRAPHQL_MIDDLEWARE_GRAPHQL_ACTIONS_URL) set, aborting")
|
|
|
|
}
|
|
|
|
|
2024-07-01 02:34:22 +08:00
|
|
|
startedAt := time.Now()
|
|
|
|
|
2024-05-02 21:45:32 +08:00
|
|
|
response, err := http.Post(graphqlActionsUrl, "application/json", bytes.NewBuffer(jsonData))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
2024-07-01 02:34:22 +08:00
|
|
|
totalDurationMillis := time.Since(startedAt).Milliseconds()
|
|
|
|
logger = logger.WithField("duration", fmt.Sprintf("%v ms", totalDurationMillis)).WithField("statusCode", response.StatusCode)
|
|
|
|
|
|
|
|
logger.Tracef("Executed!")
|
|
|
|
if totalDurationMillis > 100 {
|
|
|
|
logger.Infof("Took too long to execute!")
|
|
|
|
}
|
|
|
|
|
2024-05-02 21:45:32 +08:00
|
|
|
if response.StatusCode != 200 {
|
2024-05-22 03:11:17 +08:00
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("Error reading response body:", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var result map[string]interface{}
|
|
|
|
err = json.Unmarshal(body, &result)
|
|
|
|
if err == nil {
|
|
|
|
if message, ok := result["message"].(string); ok {
|
2024-07-01 02:34:22 +08:00
|
|
|
logger.Errorf(string(jsonData), message, err)
|
2024-05-22 03:11:17 +08:00
|
|
|
return fmt.Errorf("graphql actions request failed: %s", message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-02 21:45:32 +08:00
|
|
|
return fmt.Errorf("graphql actions request failed: %s", response.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type GqlActionsRequestBody struct {
|
|
|
|
Action GqlActionsAction `json:"action"`
|
|
|
|
Input map[string]interface{} `json:"input"`
|
|
|
|
SessionVariables map[string]string `json:"session_variables"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type GqlActionsAction struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseGraphQLMutation(query string, variables map[string]interface{}) (string, map[string]interface{}, error) {
|
|
|
|
// Extract the function name from the query
|
|
|
|
reFuncName := regexp.MustCompile(`\{\s*(\w+)`)
|
|
|
|
funcNameMatch := reFuncName.FindStringSubmatch(query)
|
|
|
|
if len(funcNameMatch) < 2 {
|
|
|
|
return "", nil, fmt.Errorf("failed to extract function name from query")
|
|
|
|
}
|
|
|
|
funcName := funcNameMatch[1]
|
|
|
|
|
|
|
|
// Prepare to extract and parse parameters
|
|
|
|
queryParams := make(map[string]interface{})
|
|
|
|
reParams := regexp.MustCompile(`\{[^(]+\(([^)]+)\)`)
|
|
|
|
paramsMatch := reParams.FindStringSubmatch(query)
|
|
|
|
if len(paramsMatch) >= 2 {
|
|
|
|
paramSplitRegex := regexp.MustCompile("[,\n]")
|
|
|
|
params := paramSplitRegex.Split(paramsMatch[1], -1)
|
|
|
|
|
|
|
|
for _, param := range params {
|
|
|
|
cleanedParam := strings.ReplaceAll(param, " ", "")
|
|
|
|
if cleanedParam == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
paramParts := strings.Split(cleanedParam, ":")
|
|
|
|
if len(paramParts) != 2 {
|
|
|
|
continue // Skip invalid params
|
|
|
|
}
|
2024-07-03 02:35:15 +08:00
|
|
|
paramName, paramValue := strings.Trim(paramParts[0], " \t"), paramParts[1]
|
2024-05-02 21:45:32 +08:00
|
|
|
|
|
|
|
// Handle variable substitution
|
|
|
|
if strings.HasPrefix(paramValue, "$") {
|
|
|
|
varName := strings.TrimPrefix(paramValue, "$")
|
|
|
|
if value, ok := variables[varName]; ok {
|
|
|
|
queryParams[paramName] = value
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
queryParams[paramName] = paramValue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return funcName, queryParams, nil
|
|
|
|
}
|