diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 86818558ea8c477d3a9e23a6f3e4207929f337f7..ef2b4637bff4438e58d9f1eaee1a699a9cf1752a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,6 +27,21 @@ coverage: stage: test script: - make coverage + after_script: + - mkdir coverage/$CI_COMMIT_BRANCH + - cp coverage/cover.html coverage/$CI_COMMIT_BRANCH + - mv coverage/$CI_COMMIT_BRANCH/cover.html coverage/$CI_COMMIT_BRANCH/index.html + # install openssh client and add ssh keys + - apt-get install openssh-client curl -y >/dev/null + - mkdir ~/.ssh/ + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - ssh-add ~/.ssh/id_rsa + - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + - ssh -fN -L 1234:science-vs260.science.uu.nl:22 sivan@up.science.uu.nl + - scp -r -o StrictHostKeyChecking=no -P 1234 -i ~/.ssh/id_rsa coverage/$CI_COMMIT_BRANCH root@localhost:/datadisk/documentation-coverage/home/backend/query-service/features artifacts: untracked: false expire_in: 30 days diff --git a/cmd/query-service/main.go b/cmd/query-service/main.go index 35cbd8e4b90e217e2e2cec4c1f730780435154da..370e4e114914282b5a598e76833661721c26eee3 100644 --- a/cmd/query-service/main.go +++ b/cmd/query-service/main.go @@ -1,7 +1,31 @@ package main -import "fmt" +import ( + "fmt" + "query-service/internal/aql" + "query-service/internal/consumer" + "query-service/internal/errorhandler" +) func main() { - fmt.Println("Helloooo world") + + exchangeID := "query-requests" + routingKey := "aql-user-request" + consumer.StartConsuming(onMessageReceived, exchangeID, routingKey) +} + +func onMessageReceived(jsonMsg *[]byte) { + // Retrieve JSON formatted string payload from msg + + // Convert the json byte msg to an aql query string + aqlQuery, err := aql.ConvertJSONToAQL(jsonMsg) + errorhandler.FailWithError(err, "failed to parse incoming msg to AQL") // TODO: don't panic on error, send error message to client instead + + fmt.Println(*aqlQuery) + + // execute and retrieve result + + // convert result to general (node-link (?)) format + + // publish converted result } diff --git a/go.mod b/go.mod index f3327accf6defbb821bf912b6f145d451654ccdc..640347d1c26a99887c1f3c39ee6d7ea7fa0e90f7 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module query-service go 1.15 + +require ( + github.com/rs/xid v1.3.0 + github.com/streadway/amqp v1.0.0 + github.com/thijsheijden/alice v0.1.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..33802db516b1ff981b34d69733c14df54d8b12a1 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= +github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/thijsheijden/alice v0.1.5 h1:kOZHhLGSHMja77I/Wvd19M7nvxgEWVIJ4vk5antCKdQ= +github.com/thijsheijden/alice v0.1.5/go.mod h1:UypS/UTucbp+fmG1JtmXGCKPnGKimAC9AgmJ8iGoLNo= diff --git a/internal/aql/aql.go b/internal/aql/aql.go new file mode 100644 index 0000000000000000000000000000000000000000..2c2bb495d7b0b56c93d6e7565e2a21cf204f1880 --- /dev/null +++ b/internal/aql/aql.go @@ -0,0 +1,293 @@ +package aql + +import ( + "encoding/json" + "fmt" + "strconv" +) + +// Constraint datatypes +// text MatchTypes: exact/contains/startswith/endswith +// number MatchTypes: GT/LT/EQ +// bool MatchTypes: EQ/NEQ + +// Ranges dus tussen 10 half 5 bijv. + +// Struct used for JSON conversion +type parsedJSON struct { + Return returnStruct + Nodes []nodeStruct + Relations []relationStruct +} + +type returnStruct struct { + Entities []int + Relations []int +} + +type nodeStruct struct { + NodeType string + Constraints map[string]constraintStruct +} +type relationStruct struct { + RelationType string + NodeFrom int + NodeTo int + Depth searchDepthStruct + Constraints map[string]constraintStruct +} +type searchDepthStruct struct { + Min int + Max int +} + +type constraintStruct struct { + Value string + DataType string + MatchType string +} + +// ConvertJSONToAQL converts a json string to an AQL query +func ConvertJSONToAQL(jsonMsg *[]byte) (*string, error) { + + jsonStruct, err := convertJSONToStruct(jsonMsg) + if err != nil { + fmt.Println(err) + return nil, err + } + + //Per node query + // per constraint + //relations koppelen ze samen + //return statement + + result := createAllNodesQuery(jsonStruct.Return.Entities, jsonStruct.Nodes) + return result, nil +} + +func createAllNodesQuery(returnEntitiesIndices []int, nodes []nodeStruct) *string { + var result string + for nodeIndex := range returnEntitiesIndices { + nodeID := fmt.Sprintf("n%s", strconv.Itoa(nodeIndex)) + + nodeQueryString := *createNodeQuery(&nodes[nodeIndex], nodeID) + + result += fmt.Sprintf(" \n%s", nodeQueryString) + } + + return &result +} + +// createNodeQuery converts the node part of the json to a subquery +func createNodeQuery(node *nodeStruct, name string) *string { + /* + LET alices = ( + FOR x IN female + FILTER x.name == "Alice" AND x.birth_year > 1997 + RETURN x + ) + + NAAR --> + + LET {NAAM**} = ( + FOR x IN {NODETYPE} + FILTER x.{CONSTRAINT[0]} {{CONSTRAINT[0]}.MATCHTYPE} {CONSTRAINT[0].VALUE} + AND x.{CONSTRAINT[1]} {{CONSTRAINT[1]}.MATCHTYPE} {CONSTRAINT[1].VALUE} + RETURN x + ) + + */ + + letStatement := fmt.Sprintf("LET %s = (\n", name) + forStatement := fmt.Sprintf("\tFOR x IN %s \n", node.NodeType) + + // Generate all constraints as FILTER statements + first := true + filter := "\tFILTER " + for key, constraint := range node.Constraints { + constraint := createQueryConstraint(&constraint, key) + + if first { + filter += fmt.Sprintf("\t%s ", *constraint) + first = false + } else { + filter += fmt.Sprintf("AND\n\t\t%s", *constraint) + } + } + + returnStatement := "\n\tRETURN x\n)" + + // Concatenate all the statements + result := letStatement + forStatement + filter + returnStatement + return &result +} + +// Constraint datatypes +// text MatchTypes: exact/contains/startswith/endswith +// number MatchTypes: GT/LT/EQ +// bool MatchTypes: EQ/NEQ + +// createQueryConstraint creates a sinlge line of AQL filtering/constraint +func createQueryConstraint(con *constraintStruct, key string) *string { + //FILTER x.{CONSTRAINT[0]} {{CONSTRAINT[0]}.MATCHTYPE} {CONSTRAINT[0].VALUE} + // name mtch val + dataType + var ( + mtch string + val string + line string + ) + + //Wicked switches letsgo + switch con.DataType { + case "text": + val = fmt.Sprintf("\"%s\"", con.Value) + switch con.MatchType { + case "contains": + mtch = "IN" + case "startswith": + mtch = "LIKE" + val = fmt.Sprintf("\"%s%%\"", con.Value) + case "endswith": + mtch = "LIKE" + val = fmt.Sprintf("\"_%s\"", con.Value) + default: //exact + mtch = "==" + } + case "number": + val = con.Value + switch con.MatchType { + case "GT": + mtch = ">" + case "LT": + mtch = "<" + case "GET": + mtch = ">=" + case "LET": + mtch = "<=" + default: //EQ + mtch = "==" + } + default: /*bool*/ + val = con.Value + switch con.MatchType { + case "NEQ": + mtch = "!=" + default: //EQ + mtch = "==" + } + } + line = fmt.Sprintf("x.%s %s %s", key, mtch, val) + return &line +} + +func convertJSONToStruct(jsonMsg *[]byte) (*parsedJSON, error) { + jsonStruct := parsedJSON{} + err := json.Unmarshal([]byte(*jsonMsg), &jsonStruct) + + if err != nil { + return nil, err + } + + return &jsonStruct, nil +} + +/* + desired output + + WITH male + FOR a1 IN female + FILTER a1.name == "Alice" + + (FOR r1v,r1e,r1p IN 1..1 OUTBOUND a1 relation + FILTER r1v.name == "Bob") + OR + (FOR r2v,r2e,r2p IN 1..1 OUTBOUND a1 relation + FILTER r2v.name == "Martin" && r2v.hasdog == true) + // constraints for Bob + OR r1v.name == "Martin" + FILTER r1e.type == "married" + + FOR r2v,r2e,r2p IN 1..1 OUTBOUND r1v relation + FILTER r2v.name == "Figo" + FILTER r2e.type == "has_dog" + + RETURN {a1,r1v,r2v} + + + of + + + LET alices = ( + FOR x IN female + FILTER x.name == "Alice" + RETURN x + ) + + LET bobs = ( + FOR x IN male + FILTER x.name == "Bob" + RETURN x + ) + + LET alices = ( + FOR alice IN alices + FOR r1v,r1e,r1p IN 1..1 OUTBOUND alice relation + FILTER r1v IN bobs AND r1e.type == "married" + RETURN {alice} + ) + + LET alices = ( + FOR alice IN alices + FOR r1v,r1e,r1p IN 1..1 OUTBOUND alice relation + FILTER r1v IN marting AND r1e.type == "friend" + RETURN {alice} + ) + + FOR a2 IN male + FILTER a2.name == "Bob" + + FOR r1v,r1e,r1p IN 1..1 OUTBOUND a1 relation + FILTER r1v._id == a2._id + FILTER r1e.type == "married" + RETURN {a1,a2,r1e} + +*/ +/* +{ + "NodeType": "female", + "Constraints": + { + "name": { "Value": "Alice", "DataType": "text", "MatchType": "exact" }, + "birth_year": { "Value": "1997", "DataType": "number", "MatchType": "GT" } + } + } + + NAAR --> + + LET alices = ( + FOR x IN female + FILTER x.name == "Alice" AND x.birth_year > 1997 + RETURN x + ) + +*/ + +//Nog een manier vinden om namen te syncen over de queries heen ** + +// func forGlory() *constraintStruct { +// var yeet constraintStruct +// yeet = constraintStruct{ +// ConstraintName: "name", +// Value: "Alice", +// MatchType: "exact", +// DataType: "text", +// } +// return &yeet +// } + +// func alsoForGlory() { +// con := forGlory() + +// toPrint := createQueryConstraint(*con) +// fmt.Println(*toPrint) +// } diff --git a/internal/consumer/consumer.go b/internal/consumer/consumer.go new file mode 100644 index 0000000000000000000000000000000000000000..e7426811a31d075b0667764f2472d9594b24ceba --- /dev/null +++ b/internal/consumer/consumer.go @@ -0,0 +1,50 @@ +package consumer + +import ( + "query-service/internal/errorhandler" + "time" + + "github.com/streadway/amqp" + "github.com/thijsheijden/alice" +) + +// ConsumeMessageFunc is a function type to be called when a message is consumed +type ConsumeMessageFunc func(*[]byte) + +// StartConsuming will start consuming messages +// When a message is received the consumeMessage function will be called +func StartConsuming(consumeMessage ConsumeMessageFunc, exchangeID string, routingKey string) { + + // Create connection config using environment variables + // rabbitUser := os.Getenv("RABBIT_USER") + // rabbitPassword := os.Getenv("RABBIT_PASSWORD") + // rabbitHost := os.Getenv("RABBIT_HOST") + // rabbitPort, err := strconv.Atoi(os.Getenv("RABBIT_PORT")) + rabbitUser := "haha-test" + rabbitPassword := "dikkedraak" + rabbitHost := "192.168.178.158" + rabbitPort := 5672 + + config := alice.CreateConfig(rabbitUser, rabbitPassword, rabbitHost, rabbitPort, true, time.Minute*1, alice.DefaultErrorHandler) + + // Open connection to broker + conn := alice.Connect(*config) + + // Declare the exchange we want to bind to + exchange, err := alice.CreateDefaultExchange(exchangeID, alice.Direct) + if err != nil { + errorhandler.FailWithError(err, "failed to create exchange") + } + + // Declare the queue we will consume from + queue := alice.CreateQueue(exchange, "", true, false, true, false, nil) + + // Create the consumer + c, err := conn.CreateConsumer(queue, routingKey, alice.DefaultConsumerErrorHandler) + if err != nil { + errorhandler.FailWithError(err, "failed to create consumer") + } + + // Start consuming messages + c.ConsumeMessages(nil, false, func(msg amqp.Delivery) { consumeMessage(&msg.Body) }) +} diff --git a/internal/errorhandler/errorhandler.go b/internal/errorhandler/errorhandler.go new file mode 100644 index 0000000000000000000000000000000000000000..98047a959e69fdc6e191e2aab660a858be6bf8d3 --- /dev/null +++ b/internal/errorhandler/errorhandler.go @@ -0,0 +1,20 @@ +package errorhandler + +import ( + "fmt" +) + +// LogError logs an error that is not nil +func LogError(err error, msg string) { + if err != nil { + fmt.Printf("%s: %v", msg, err) + } +} + +// FailWithError panics if the error is not nil +func FailWithError(err error, msg string) { + if err != nil { + fmt.Printf("%s: %v", msg, err) + panic(err) + } +}