diff --git a/.gitignore b/.gitignore index 55fd5e32e74ea895434c02b92ad70ca3811794de..b3442db2f07649c1182f3cb879d73f4661967d02 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -builds/ \ No newline at end of file +builds/ +cover.html +cover.out diff --git a/integration-testing/config.json b/integration-testing/config.json index 6ee18939dc2e4625c3e66b6831211326bf882d8f..be98942096c77e10172dc9b3d8d3870dd90a19a7 100644 --- a/integration-testing/config.json +++ b/integration-testing/config.json @@ -11,7 +11,14 @@ "headers": { "sessionID": "test-session" }, - "data":"{'Return':{'Entities':[0,1],'Relations':[0]},'Entities':[{'Type':'airports','Constraints':[{'Attribute':'city','Value':'New York','DataType':'text','MatchType':'exact'}]},{'Type':'airports','Constraints':[{'Attribute':'city','Value':'San Francisco','DataType':'text','MatchType':'exact'},{'Attribute':'vip','Value':'true','DataType':'bool','MatchType':'exact'}]}],'Relations':[{'Type':'flights','Depth':{'min':1,'max':1},'EntityFrom':0,'EntityTo':1,'Constraints':[{'Attribute':'Month','Value':'1','DataType':'number','MatchType':'exact'},{'Attribute':'Day','Value':'15','DataType':'number','MatchType':'exact'}]}], 'limit': 1000}" + "data":"This is not a valid query" + }, + { + "routingKey": "aql-query-request", + "headers": { + "sessionID": "test-session" + }, + "data":"{\"Return\":{\"Entities\":[0,1],\"Relations\":[0]},\"Entities\":[{\"Type\":\"airports\",\"Constraints\":[{\"Attribute\":\"city\",\"Value\":\"New York\",\"DataType\":\"text\",\"MatchType\":\"exact\"}]},{\"Type\":\"airports\",\"Constraints\":[{\"Attribute\":\"city\",\"Value\":\"San Francisco\",\"DataType\":\"text\",\"MatchType\":\"exact\"},{\"Attribute\":\"vip\",\"Value\":\"true\",\"DataType\":\"bool\",\"MatchType\":\"exact\"}]}],\"Relations\":[{\"Type\":\"flights\",\"Depth\":{\"min\":1,\"max\":1},\"EntityFrom\":0,\"EntityTo\":1,\"Constraints\":[{\"Attribute\":\"Month\",\"Value\":\"1\",\"DataType\":\"number\",\"MatchType\":\"exact\"},{\"Attribute\":\"Day\",\"Value\":\"15\",\"DataType\":\"number\",\"MatchType\":\"exact\"}]}], \"limit\": 1000}" } ] } @@ -30,13 +37,19 @@ "type": "exact", "consumerid": "consumer1", "id": "query result returned", - "data": "{'type':'query_result','values':{'edges':[{'_from':'airports/JFK','_id':'flights/286552','_key':'286552','_rev':'_cLqg0be--U','_to':'airports/SFO','attributes':{'ArrTime':2332,'ArrTimeUTC':'2008-01-16T07:32:00.000Z','Day':15,'DayOfWeek':2,'DepTime':2006,'DepTimeUTC':'2008-01-16T01:06:00.000Z','Distance':2586,'FlightNum':649,'Month':1,'TailNum':'N597JB','UniqueCarrier':'B6','Year':2008}},{'_from':'airports/JFK','_id':'flights/286394','_key':'286394','_rev':'_cLqg0bG--s','_to':'airports/SFO','attributes':{'ArrTime':2330,'ArrTimeUTC':'2008-01-16T07:30:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1958,'DepTimeUTC':'2008-01-16T00:58:00.000Z','Distance':2586,'FlightNum':19,'Month':1,'TailNum':'N537UA','UniqueCarrier':'UA','Year':2008}},{'_from':'airports/JFK','_id':'flights/283386','_key':'283386','_rev':'_cLqg0Ty--g','_to':'airports/SFO','attributes':{'ArrTime':2215,'ArrTimeUTC':'2008-01-16T06:15:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1849,'DepTimeUTC':'2008-01-15T23:49:00.000Z','Distance':2586,'FlightNum':73,'Month':1,'TailNum':'N3759','UniqueCarrier':'DL','Year':2008}},{'_from':'airports/JFK','_id':'flights/282498','_key':'282498','_rev':'_cLqg0Rm--Y','_to':'airports/SFO','attributes':{'ArrTime':2358,'ArrTimeUTC':'2008-01-16T07:58:00.000Z','Day':15,'DayOfWeek':2,'DepTime':2052,'DepTimeUTC':'2008-01-16T01:52:00.000Z','Distance':2586,'FlightNum':9,'Month':1,'TailNum':'N554UA','UniqueCarrier':'UA','Year':2008}},{'_from':'airports/JFK','_id':'flights/282321','_key':'282321','_rev':'_cLqg0RK--s','_to':'airports/SFO','attributes':{'ArrTime':2132,'ArrTimeUTC':'2008-01-16T05:32:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1825,'DepTimeUTC':'2008-01-15T23:25:00.000Z','Distance':2586,'FlightNum':647,'Month':1,'TailNum':'N651JB','UniqueCarrier':'B6','Year':2008}},{'_from':'airports/JFK','_id':'flights/281151','_key':'281151','_rev':'_cLqg0Oe--M','_to':'airports/SFO','attributes':{'ArrTime':2108,'ArrTimeUTC':'2008-01-16T05:08:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1746,'DepTimeUTC':'2008-01-15T22:46:00.000Z','Distance':2586,'FlightNum':17,'Month':1,'TailNum':'N560UA','UniqueCarrier':'UA','Year':2008}},{'_from':'airports/JFK','_id':'flights/281077','_key':'281077','_rev':'_cLqg0OS--S','_to':'airports/SFO','attributes':{'ArrTime':2119,'ArrTimeUTC':'2008-01-16T05:19:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1742,'DepTimeUTC':'2008-01-15T22:42:00.000Z','Distance':2586,'FlightNum':177,'Month':1,'TailNum':'N321AA','UniqueCarrier':'AA','Year':2008}},{'_from':'airports/JFK','_id':'flights/279990','_key':'279990','_rev':'_cLqg0Lq--m','_to':'airports/SFO','attributes':{'ArrTime':1423,'ArrTimeUTC':'2008-01-15T22:23:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1114,'DepTimeUTC':'2008-01-15T16:14:00.000Z','Distance':2586,'FlightNum':11,'Month':1,'TailNum':'N555UA','UniqueCarrier':'UA','Year':2008}},{'_from':'airports/JFK','_id':'flights/279206','_key':'279206','_rev':'_cLqg0Jy--U','_to':'airports/SFO','attributes':{'ArrTime':1406,'ArrTimeUTC':'2008-01-15T22:06:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1054,'DepTimeUTC':'2008-01-15T15:54:00.000Z','Distance':2586,'FlightNum':15,'Month':1,'TailNum':'N356AA','UniqueCarrier':'AA','Year':2008}},{'_from':'airports/JFK','_id':'flights/278458','_key':'278458','_rev':'_cLqg0H6--s','_to':'airports/SFO','attributes':{'ArrTime':2003,'ArrTimeUTC':'2008-01-16T04:03:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1624,'DepTimeUTC':'2008-01-15T21:24:00.000Z','Distance':2586,'FlightNum':151,'Month':1,'TailNum':'N3744D','UniqueCarrier':'DL','Year':2008}},{'_from':'airports/JFK','_id':'flights/276601','_key':'276601','_rev':'_cLqg0Dq--O','_to':'airports/SFO','attributes':{'ArrTime':1909,'ArrTimeUTC':'2008-01-16T03:09:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1535,'DepTimeUTC':'2008-01-15T20:35:00.000Z','Distance':2586,'FlightNum':15,'Month':1,'TailNum':'N510UA','UniqueCarrier':'UA','Year':2008}},{'_from':'airports/JFK','_id':'flights/276139','_key':'276139','_rev':'_cLqg0Ci--i','_to':'airports/SFO','attributes':{'ArrTime':1815,'ArrTimeUTC':'2008-01-16T02:15:00.000Z','Day':15,'DayOfWeek':2,'DepTime':1524,'DepTimeUTC':'2008-01-15T20:24:00.000Z','Distance':2586,'FlightNum':85,'Month':1,'TailNum':'N373AA','UniqueCarrier':'AA','Year':2008}},{'_from':'airports/JFK','_id':'flights/273773','_key':'273773','_rev':'_cLqgz86--K','_to':'airports/SFO','attributes':{'ArrTime':1156,'ArrTimeUTC':'2008-01-15T19:56:00.000Z','Day':15,'DayOfWeek':2,'DepTime':840,'DepTimeUTC':'2008-01-15T13:40:00.000Z','Distance':2586,'FlightNum':2979,'Month':1,'TailNum':'N387AA','UniqueCarrier':'AA','Year':2008}},{'_from':'airports/JFK','_id':'flights/273538','_key':'273538','_rev':'_cLqgz8W--k','_to':'airports/SFO','attributes':{'ArrTime':1131,'ArrTimeUTC':'2008-01-15T19:31:00.000Z','Day':15,'DayOfWeek':2,'DepTime':831,'DepTimeUTC':'2008-01-15T13:31:00.000Z','Distance':2586,'FlightNum':893,'Month':1,'TailNum':'N505UA','UniqueCarrier':'UA','Year':2008}},{'_from':'airports/JFK','_id':'flights/272322','_key':'272322','_rev':'_cLqgz5q--g','_to':'airports/SFO','attributes':{'ArrTime':1118,'ArrTimeUTC':'2008-01-15T19:18:00.000Z','Day':15,'DayOfWeek':2,'DepTime':757,'DepTimeUTC':'2008-01-15T12:57:00.000Z','Distance':2586,'FlightNum':641,'Month':1,'TailNum':'N528JB','UniqueCarrier':'B6','Year':2008}},{'_from':'airports/JFK','_id':'flights/271860','_key':'271860','_rev':'_cLqgz4q--G','_to':'airports/SFO','attributes':{'ArrTime':1133,'ArrTimeUTC':'2008-01-15T19:33:00.000Z','Day':15,'DayOfWeek':2,'DepTime':743,'DepTimeUTC':'2008-01-15T12:43:00.000Z','Distance':2586,'FlightNum':877,'Month':1,'TailNum':'N502UA','UniqueCarrier':'UA','Year':2008}},{'_from':'airports/JFK','_id':'flights/270140','_key':'270140','_rev':'_cLqgz02--K','_to':'airports/SFO','attributes':{'ArrTime':1042,'ArrTimeUTC':'2008-01-15T18:42:00.000Z','Day':15,'DayOfWeek':2,'DepTime':650,'DepTimeUTC':'2008-01-15T11:50:00.000Z','Distance':2586,'FlightNum':59,'Month':1,'TailNum':'N399AA','UniqueCarrier':'AA','Year':2008}},{'_from':'airports/JFK','_id':'flights/269347','_key':'269347','_rev':'_cLqgzz---A','_to':'airports/SFO','attributes':{'ArrTime':913,'ArrTimeUTC':'2008-01-15T17:13:00.000Z','Day':15,'DayOfWeek':2,'DepTime':606,'DepTimeUTC':'2008-01-15T11:06:00.000Z','Distance':2586,'FlightNum':5,'Month':1,'TailNum':'N537UA','UniqueCarrier':'UA','Year':2008}}],'nodes':[{'_id':'airports/SFO','_key':'SFO','_rev':'_cLp3eL6-_G','attributes':{'city':'San Francisco','country':'USA','lat':37.61900194,'long':-122.3748433,'name':'San Francisco International','state':'CA','vip':true}},{'_id':'airports/JFK','_key':'JFK','_rev':'_cLp3eLO-_I','attributes':{'city':'New York','country':'USA','lat':40.63975111,'long':-73.77892556,'name':'John F Kennedy Intl','state':'NY','vip':true}}]}}" + "data": "{\"type\":\"query_result\",\"values\":{\"edges\":[{\"_from\":\"airports/JFK\",\"_id\":\"flights/286552\",\"_key\":\"286552\",\"_rev\":\"_cLqg0be--U\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":2332,\"ArrTimeUTC\":\"2008-01-16T07:32:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":2006,\"DepTimeUTC\":\"2008-01-16T01:06:00.000Z\",\"Distance\":2586,\"FlightNum\":649,\"Month\":1,\"TailNum\":\"N597JB\",\"UniqueCarrier\":\"B6\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/286394\",\"_key\":\"286394\",\"_rev\":\"_cLqg0bG--s\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":2330,\"ArrTimeUTC\":\"2008-01-16T07:30:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1958,\"DepTimeUTC\":\"2008-01-16T00:58:00.000Z\",\"Distance\":2586,\"FlightNum\":19,\"Month\":1,\"TailNum\":\"N537UA\",\"UniqueCarrier\":\"UA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/283386\",\"_key\":\"283386\",\"_rev\":\"_cLqg0Ty--g\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":2215,\"ArrTimeUTC\":\"2008-01-16T06:15:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1849,\"DepTimeUTC\":\"2008-01-15T23:49:00.000Z\",\"Distance\":2586,\"FlightNum\":73,\"Month\":1,\"TailNum\":\"N3759\",\"UniqueCarrier\":\"DL\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/282498\",\"_key\":\"282498\",\"_rev\":\"_cLqg0Rm--Y\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":2358,\"ArrTimeUTC\":\"2008-01-16T07:58:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":2052,\"DepTimeUTC\":\"2008-01-16T01:52:00.000Z\",\"Distance\":2586,\"FlightNum\":9,\"Month\":1,\"TailNum\":\"N554UA\",\"UniqueCarrier\":\"UA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/282321\",\"_key\":\"282321\",\"_rev\":\"_cLqg0RK--s\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":2132,\"ArrTimeUTC\":\"2008-01-16T05:32:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1825,\"DepTimeUTC\":\"2008-01-15T23:25:00.000Z\",\"Distance\":2586,\"FlightNum\":647,\"Month\":1,\"TailNum\":\"N651JB\",\"UniqueCarrier\":\"B6\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/281151\",\"_key\":\"281151\",\"_rev\":\"_cLqg0Oe--M\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":2108,\"ArrTimeUTC\":\"2008-01-16T05:08:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1746,\"DepTimeUTC\":\"2008-01-15T22:46:00.000Z\",\"Distance\":2586,\"FlightNum\":17,\"Month\":1,\"TailNum\":\"N560UA\",\"UniqueCarrier\":\"UA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/281077\",\"_key\":\"281077\",\"_rev\":\"_cLqg0OS--S\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":2119,\"ArrTimeUTC\":\"2008-01-16T05:19:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1742,\"DepTimeUTC\":\"2008-01-15T22:42:00.000Z\",\"Distance\":2586,\"FlightNum\":177,\"Month\":1,\"TailNum\":\"N321AA\",\"UniqueCarrier\":\"AA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/279990\",\"_key\":\"279990\",\"_rev\":\"_cLqg0Lq--m\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1423,\"ArrTimeUTC\":\"2008-01-15T22:23:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1114,\"DepTimeUTC\":\"2008-01-15T16:14:00.000Z\",\"Distance\":2586,\"FlightNum\":11,\"Month\":1,\"TailNum\":\"N555UA\",\"UniqueCarrier\":\"UA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/279206\",\"_key\":\"279206\",\"_rev\":\"_cLqg0Jy--U\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1406,\"ArrTimeUTC\":\"2008-01-15T22:06:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1054,\"DepTimeUTC\":\"2008-01-15T15:54:00.000Z\",\"Distance\":2586,\"FlightNum\":15,\"Month\":1,\"TailNum\":\"N356AA\",\"UniqueCarrier\":\"AA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/278458\",\"_key\":\"278458\",\"_rev\":\"_cLqg0H6--s\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":2003,\"ArrTimeUTC\":\"2008-01-16T04:03:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1624,\"DepTimeUTC\":\"2008-01-15T21:24:00.000Z\",\"Distance\":2586,\"FlightNum\":151,\"Month\":1,\"TailNum\":\"N3744D\",\"UniqueCarrier\":\"DL\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/276601\",\"_key\":\"276601\",\"_rev\":\"_cLqg0Dq--O\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1909,\"ArrTimeUTC\":\"2008-01-16T03:09:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1535,\"DepTimeUTC\":\"2008-01-15T20:35:00.000Z\",\"Distance\":2586,\"FlightNum\":15,\"Month\":1,\"TailNum\":\"N510UA\",\"UniqueCarrier\":\"UA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/276139\",\"_key\":\"276139\",\"_rev\":\"_cLqg0Ci--i\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1815,\"ArrTimeUTC\":\"2008-01-16T02:15:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":1524,\"DepTimeUTC\":\"2008-01-15T20:24:00.000Z\",\"Distance\":2586,\"FlightNum\":85,\"Month\":1,\"TailNum\":\"N373AA\",\"UniqueCarrier\":\"AA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/273773\",\"_key\":\"273773\",\"_rev\":\"_cLqgz86--K\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1156,\"ArrTimeUTC\":\"2008-01-15T19:56:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":840,\"DepTimeUTC\":\"2008-01-15T13:40:00.000Z\",\"Distance\":2586,\"FlightNum\":2979,\"Month\":1,\"TailNum\":\"N387AA\",\"UniqueCarrier\":\"AA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/273538\",\"_key\":\"273538\",\"_rev\":\"_cLqgz8W--k\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1131,\"ArrTimeUTC\":\"2008-01-15T19:31:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":831,\"DepTimeUTC\":\"2008-01-15T13:31:00.000Z\",\"Distance\":2586,\"FlightNum\":893,\"Month\":1,\"TailNum\":\"N505UA\",\"UniqueCarrier\":\"UA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/272322\",\"_key\":\"272322\",\"_rev\":\"_cLqgz5q--g\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1118,\"ArrTimeUTC\":\"2008-01-15T19:18:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":757,\"DepTimeUTC\":\"2008-01-15T12:57:00.000Z\",\"Distance\":2586,\"FlightNum\":641,\"Month\":1,\"TailNum\":\"N528JB\",\"UniqueCarrier\":\"B6\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/271860\",\"_key\":\"271860\",\"_rev\":\"_cLqgz4q--G\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1133,\"ArrTimeUTC\":\"2008-01-15T19:33:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":743,\"DepTimeUTC\":\"2008-01-15T12:43:00.000Z\",\"Distance\":2586,\"FlightNum\":877,\"Month\":1,\"TailNum\":\"N502UA\",\"UniqueCarrier\":\"UA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/270140\",\"_key\":\"270140\",\"_rev\":\"_cLqgz02--K\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":1042,\"ArrTimeUTC\":\"2008-01-15T18:42:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":650,\"DepTimeUTC\":\"2008-01-15T11:50:00.000Z\",\"Distance\":2586,\"FlightNum\":59,\"Month\":1,\"TailNum\":\"N399AA\",\"UniqueCarrier\":\"AA\",\"Year\":2008}},{\"_from\":\"airports/JFK\",\"_id\":\"flights/269347\",\"_key\":\"269347\",\"_rev\":\"_cLqgzz---A\",\"_to\":\"airports/SFO\",\"attributes\":{\"ArrTime\":913,\"ArrTimeUTC\":\"2008-01-15T17:13:00.000Z\",\"Day\":15,\"DayOfWeek\":2,\"DepTime\":606,\"DepTimeUTC\":\"2008-01-15T11:06:00.000Z\",\"Distance\":2586,\"FlightNum\":5,\"Month\":1,\"TailNum\":\"N537UA\",\"UniqueCarrier\":\"UA\",\"Year\":2008}}],\"nodes\":[{\"_id\":\"airports/SFO\",\"_key\":\"SFO\",\"_rev\":\"_cLp3eL6-_G\",\"attributes\":{\"city\":\"San Francisco\",\"country\":\"USA\",\"lat\":37.61900194,\"long\":-122.3748433,\"name\":\"San Francisco International\",\"state\":\"CA\",\"vip\":true}},{\"_id\":\"airports/JFK\",\"_key\":\"JFK\",\"_rev\":\"_cLp3eLO-_I\",\"attributes\":{\"city\":\"New York\",\"country\":\"USA\",\"lat\":40.63975111,\"long\":-73.77892556,\"name\":\"John F Kennedy Intl\",\"state\":\"NY\",\"vip\":true}}]}}" }, { "type": "exact", "consumerid": "consumer1", "id": "query translation result returned", - "data": "{'type':'query_translation_result','values':'LET n0 = (FOR x IN airports FILTER x.city == \\\"New York\\\" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: \\\"path\\\" }FILTER v.city == \\\"San Francisco\\\" AND v.vip == true FILTER p.edges[*].Month ALL == 1 AND p.edges[*].Day ALL == 15 LIMIT 1000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {\\\"vertices\\\":nodes, \\\"edges\\\":edges }'}" + "data": "{\"type\":\"query_translation_result\",\"values\":\"LET n0 = (FOR x IN airports FILTER x.city == \\\"New York\\\" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: \\\"path\\\" }FILTER v.city == \\\"San Francisco\\\" AND v.vip == true FILTER p.edges[*].Month ALL == 1 AND p.edges[*].Day ALL == 15 LIMIT 1000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {\\\"vertices\\\":nodes, \\\"edges\\\":edges }\"}" + }, + { + "type": "exact", + "consumerid": "consumer1", + "id": "query translation error returned", + "data": "{\"type\":\"query_translation_error\",\"value\":\"invalid character 'T' looking for beginning of value\"}" } ], "redisSet": { diff --git a/internal/drivers/brokerdriver/interface.go b/internal/drivers/brokerdriver/interface.go index 3aacf64fea45b25f2eb1dd9c89792ceecbf8192f..3dc54da2fc47fbae03fcf60d59c5dedcf8f296f6 100644 --- a/internal/drivers/brokerdriver/interface.go +++ b/internal/drivers/brokerdriver/interface.go @@ -20,5 +20,5 @@ type Consumer interface { // A Producer belongs to a broker and publishes messages to a queue type Producer interface { - PublishMessage(body *[]byte, queueID *string, headers *amqp.Table) + PublishMessage(body *[]byte, routingKey *string, headers *amqp.Table) } diff --git a/internal/drivers/brokerdriver/mock/broker.go b/internal/drivers/brokerdriver/mock/broker.go index 8483d8fdc1f2b4d2d25061596854daf041d79393..80f1c1c89637212a46c18cd1fd89fc32a2a6c8f7 100644 --- a/internal/drivers/brokerdriver/mock/broker.go +++ b/internal/drivers/brokerdriver/mock/broker.go @@ -8,18 +8,32 @@ import ( // Driver is mock gateway type Driver struct { gateway brokeradapter.GatewayInterface + + // Mock messages that are published by producers on this broker + // Key is the routing key + // Value is a slice of messages, in order of being sent 'first -> last' + Messages map[string][]brokeradapter.Message } -// CreateBroker is a creates a mock driver +// CreateBroker creates a broker driver (mock) func CreateBroker(gateway brokeradapter.GatewayInterface) *Driver { return &Driver{ - gateway: gateway, + gateway: gateway, + Messages: make(map[string][]brokeradapter.Message), } } -// CreateConsumer creates a mock consumer +// CreateConsumer creates a consumer (mock) func (d *Driver) CreateConsumer() brokerdriver.Consumer { return &Consumer{ broker: d, } } + +// CreateProducer creates a producer (mock) +func (d *Driver) CreateProducer() brokerdriver.Producer { + return &Producer{ + broker: d, + exchange: "ui-direct-exchange", // This is the only exchange this service produces to + } +} diff --git a/internal/drivers/brokerdriver/mock/consumer.go b/internal/drivers/brokerdriver/mock/consumer.go index b3391265198146245752370f2f559524e73df65b..9118043873bffca0bad53758fc7c109361cf409c 100644 --- a/internal/drivers/brokerdriver/mock/consumer.go +++ b/internal/drivers/brokerdriver/mock/consumer.go @@ -2,17 +2,17 @@ package mockbrokerdriver import "query-service/internal/adapters/brokeradapter" -// Consumer is a mock consumer +// A Consumer implements the consumer interface (mock) type Consumer struct { broker *Driver } -// ConsumeMessages mocks the consume messages func +// ConsumeMessages consumes messages from the broker (mock) func (c *Consumer) ConsumeMessages() { } -// SetMessageHandler mocks the setting of a message handler +// SetMessageHandler mocks the setting of a message handler (mock) func (c *Consumer) SetMessageHandler(handler func(msg *brokeradapter.Message)) { } diff --git a/internal/drivers/brokerdriver/mock/producer.go b/internal/drivers/brokerdriver/mock/producer.go new file mode 100644 index 0000000000000000000000000000000000000000..1367971d059641fec4a3c0adcec28cf6d4591bd3 --- /dev/null +++ b/internal/drivers/brokerdriver/mock/producer.go @@ -0,0 +1,27 @@ +package mockbrokerdriver + +import ( + "query-service/internal/adapters/brokeradapter" + + "github.com/streadway/amqp" +) + +// A Producer implements the producer interface (mock) +type Producer struct { + broker *Driver + + // The exchange this producer is connected to + exchange string +} + +// PublishMessage publishes a message to the given queue (mock) +func (p *Producer) PublishMessage(body *[]byte, routingKey *string, headers *amqp.Table) { + // Create the message + msg := brokeradapter.Message{ + Headers: *headers, + Body: *body, + } + + // Append the message to the list + p.broker.Messages[*routingKey] = append(p.broker.Messages[*routingKey], msg) +} diff --git a/internal/drivers/brokerdriver/producer.go b/internal/drivers/brokerdriver/producer.go index e037aac06b3c6e2911851f722dd17017b5c21653..513e43d41b55110590092c0c9dc7479b7dd83402 100644 --- a/internal/drivers/brokerdriver/producer.go +++ b/internal/drivers/brokerdriver/producer.go @@ -14,10 +14,10 @@ type AliceProducer struct { producer alice.Producer } -// PublishMessage will publish a message to the specified queue id -func (ap *AliceProducer) PublishMessage(body *[]byte, queueID *string, headers *amqp.Table) { +// PublishMessage will publish a message to the specified queue id (mock) +func (ap *AliceProducer) PublishMessage(body *[]byte, routingKey *string, headers *amqp.Table) { sessionID := (*headers)["sessionID"] - logger.Log(fmt.Sprintf("Publishing message to queue %v, for session %v", *queueID, sessionID)) + logger.Log(fmt.Sprintf("Publishing message to queue %v, for session %v", *routingKey, sessionID)) - ap.producer.PublishMessage(*body, queueID, headers) + ap.producer.PublishMessage(*body, routingKey, headers) } diff --git a/internal/drivers/keyvaluedriver/interface.go b/internal/drivers/keyvaluedriver/interface.go index ad465b3f7f4212470076b09d9edaa38edab9892d..9c2c98c3d1e0606e253c9cc381e3ed034e37a06c 100644 --- a/internal/drivers/keyvaluedriver/interface.go +++ b/internal/drivers/keyvaluedriver/interface.go @@ -1,7 +1,7 @@ package keyvaluedriver -// KeyValueStore is an interface for a key value storage -type KeyValueStore interface { - Get(key *string) *string - Set(key *string, value interface{}) error +// KeyValueStoreInterface is an interface for a key value storage +type KeyValueStoreInterface interface { + Get(key *string) string + Set(key *string, value *string) error } diff --git a/internal/drivers/keyvaluedriver/redisdriver.go b/internal/drivers/keyvaluedriver/keyvaluedriver.go similarity index 63% rename from internal/drivers/keyvaluedriver/redisdriver.go rename to internal/drivers/keyvaluedriver/keyvaluedriver.go index c3420dd52d3196bdced96d97e8de3d30dccfd028..15c3595072b3b1b4229cea9d48b58890f1931d59 100644 --- a/internal/drivers/keyvaluedriver/redisdriver.go +++ b/internal/drivers/keyvaluedriver/keyvaluedriver.go @@ -9,18 +9,18 @@ import ( "github.com/go-redis/redis/v8" ) -// RedisDriver models the redis driver -type RedisDriver struct { +// KeyValueDriver models the redis driver +type KeyValueDriver struct { client *redis.Client } // NewRedisDriver creates and returns a redis driver -func NewRedisDriver() *RedisDriver { - return &RedisDriver{} +func NewRedisDriver() *KeyValueDriver { + return &KeyValueDriver{} } // Start starts the redis driver -func (d *RedisDriver) Start() { +func (d *KeyValueDriver) Start() { // Grab the redis host and port from environment vars redisAddress := os.Getenv("REDIS_ADDRESS") // redisPassword := os.Getenv("REDIS_PASSWORD") @@ -35,13 +35,12 @@ func (d *RedisDriver) Start() { } // Get retrieves the value from the redis store that belongs to the given key -func (d *RedisDriver) Get(key *string) *string { - value := d.client.Get(context.Background(), *key).Val() - return &value +func (d *KeyValueDriver) Get(key *string) string { + return d.client.Get(context.Background(), *key).Val() } // Set sets the key value pair in the redis store -func (d *RedisDriver) Set(key *string, value interface{}) error { - status := d.client.Set(context.Background(), *key, value, 0) +func (d *KeyValueDriver) Set(key *string, value *string) error { + status := d.client.Set(context.Background(), *key, *value, 0) return status.Err() } diff --git a/internal/drivers/keyvaluedriver/mock/mockkeyvaluedriver.go b/internal/drivers/keyvaluedriver/mock/mockkeyvaluedriver.go new file mode 100644 index 0000000000000000000000000000000000000000..981b7cdacb6d455d966913406dbe9569fed896df --- /dev/null +++ b/internal/drivers/keyvaluedriver/mock/mockkeyvaluedriver.go @@ -0,0 +1,24 @@ +package mockkeyvaluedriver + +// A KeyValueStore implements methods to set key-value data (mock) +type KeyValueStore struct { + data map[string]string +} + +// CreateKeyValueStore creates a key value store driver (mock) +func CreateKeyValueStore() *KeyValueStore { + return &KeyValueStore{ + data: make(map[string]string), + } +} + +// Set sets a key to a value in the key value store. Expects a non-pointer as value. (mock) +func (kvs *KeyValueStore) Set(key *string, value *string) error { + kvs.data[*key] = *value + return nil +} + +// Get gets the value for the supplied key from the key value store (mock) +func (kvs *KeyValueStore) Get(key *string) string { + return kvs.data[*key] +} diff --git a/internal/entity/document.go b/internal/entity/document.go new file mode 100644 index 0000000000000000000000000000000000000000..08ef36d21bf6d9943034e2080d49b8110e621266 --- /dev/null +++ b/internal/entity/document.go @@ -0,0 +1,13 @@ +package entity + +// Document with Empty struct to retrieve all data from the DB Document +type Document map[string]interface{} + +// GeneralFormat with Empty struct to retrieve all data from the DB Document +type GeneralFormat map[string][]Document + +// ListContainer is a struct that keeps track of the nodes and edges that need to be returned +type ListContainer struct { + NodeList []Document + EdgeList []Document +} diff --git a/internal/entity/querystruct.go b/internal/entity/querystruct.go new file mode 100644 index 0000000000000000000000000000000000000000..8e3d4f28fc6bcf4a0e502d871db3fca36fce1b2d --- /dev/null +++ b/internal/entity/querystruct.go @@ -0,0 +1,53 @@ +package entity + +// QueryParsedJSON is used for JSON conversion of the incoming byte array +type QueryParsedJSON struct { + Return QueryReturnStruct + Entities []QueryEntityStruct + Relations []QueryRelationStruct + + // Limit is for limiting the amount of paths AQL will return in a relation let statement + Limit int +} + +// QueryReturnStruct holds the indices of the entities and relations that need to be returned +type QueryReturnStruct struct { + Entities []int + Relations []int +} + +// QueryEntityStruct encapsulates a single entity with its corresponding constraints +type QueryEntityStruct struct { + Type string + Constraints []QueryConstraintStruct +} + +// QueryRelationStruct encapsulates a single relation with its corresponding constraints +type QueryRelationStruct struct { + Type string + EntityFrom int + EntityTo int + Depth QuerySearchDepthStruct + Constraints []QueryConstraintStruct +} + +// QuerySearchDepthStruct holds the range of traversals for the relation +type QuerySearchDepthStruct struct { + Min int + Max int +} + +/* +QueryConstraintStruct holds the information of the constraint + +Constraint datatypes + text MatchTypes: exact/contains/startswith/endswith + number MatchTypes: GT/LT/EQ + bool MatchTypes: EQ/NEQ +*/ +type QueryConstraintStruct struct { + Attribute string + Value string + DataType string + MatchType string +} diff --git a/internal/usecases/consume/consume.go b/internal/usecases/consume/consume.go index 2ccb2e3bbdce20cbd2ddfcea32202a36ca1752f5..af9cd8f6427f868f93e5cd65c1b31bb941160e71 100644 --- a/internal/usecases/consume/consume.go +++ b/internal/usecases/consume/consume.go @@ -17,11 +17,11 @@ type Service struct { } // NewService creates a new service -func NewService(broker brokerdriver.Broker, produceService produce.UseCase, converQueryService convertquery.UseCase, requestSenderService request.UseCase) *Service { +func NewService(broker brokerdriver.Broker, produceService produce.UseCase, convertQueryService convertquery.UseCase, requestSenderService request.UseCase) *Service { return &Service{ broker: broker, producer: produceService, - queryConverter: converQueryService, + queryConverter: convertQueryService, requestSender: requestSenderService, } } diff --git a/internal/usecases/consume/consume_test.go b/internal/usecases/consume/consume_test.go new file mode 100644 index 0000000000000000000000000000000000000000..acea44e1662df3eaf51d51fec2558f5391e79e90 --- /dev/null +++ b/internal/usecases/consume/consume_test.go @@ -0,0 +1,215 @@ +package consume + +import ( + "encoding/json" + "query-service/internal/adapters/brokeradapter" + mockbrokerdriver "query-service/internal/drivers/brokerdriver/mock" + mockkeyvaluedriver "query-service/internal/drivers/keyvaluedriver/mock" + mockconvertquery "query-service/internal/usecases/convertquery/mock" + "query-service/internal/usecases/produce" + mockrequest "query-service/internal/usecases/request/mock" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStart(t *testing.T) { + // // Create broker adapter + // brokerAdapter := brokeradapter.CreateGateway() + // // Create a mock broker + // mockBroker := mockbrokerdriver.CreateBroker(brokerAdapter) + // // Create mock key value store + // keyValueStore := mockkeyvaluedriver.CreateKeyValueStore() + // // Create new producer service + // producerService := produce.NewService(mockBroker, keyValueStore) + // // Create new convert query service + // convertQueryService := convertquery.NewService() + // // Create new request sender service + // requestSenderService := request.NewService() + // // Create new service + // service := NewService(mockBroker, producerService, convertQueryService, requestSenderService) +} + +func TestHandleCorrectMessage(t *testing.T) { + // Create broker adapter + brokerAdapter := brokeradapter.CreateGateway() + // Create a mock broker + mockBroker := mockbrokerdriver.CreateBroker(brokerAdapter) + // Create mock key value store + keyValueStore := mockkeyvaluedriver.CreateKeyValueStore() + // Create new producer service + producerService := produce.NewService(mockBroker, keyValueStore) + producerService.Start() + // Create new convert query service + convertQueryService := mockconvertquery.NewService() + // Create new request sender service + requestSenderService := mockrequest.NewService() + // Create new service + service := NewService(mockBroker, producerService, convertQueryService, requestSenderService) + + // Create mock session and mock queue + mockSession := "mock-session" + mockQueue := "mock-queue" + + // Set the test-session sessionID queue to mock-queue in key value store + keyValueStore.Set(&mockSession, &mockQueue) + + // Create headers containing a sessionID + headers := make(map[string]interface{}) + headers["sessionID"] = mockSession + mockMessage := brokeradapter.Message{ + Headers: headers, + Body: []byte("test message"), + } + + // Assert that there have not been any messages sent yet + assert.Empty(t, mockBroker.Messages) + + // Send the mock message + service.HandleMessage(&mockMessage) + + // Assert that there now are two messages that have been sent with routing key mock-queue + assert.Len(t, mockBroker.Messages[mockQueue], 2) + + // Assert that the first message is of type 'query_translation_result' and has 'Query converted' as value + var translationMessage map[string]interface{} + json.Unmarshal(mockBroker.Messages[mockQueue][0].Body, &translationMessage) + assert.Equal(t, "query_translation_result", translationMessage["type"]) + assert.Equal(t, "Query converted", translationMessage["values"]) + + // Assert that the second message is of type 'query_result' and contains no values + var resultMessage map[string]interface{} + json.Unmarshal(mockBroker.Messages[mockQueue][1].Body, &resultMessage) + assert.Equal(t, "query_result", resultMessage["type"]) + assert.Empty(t, resultMessage["values"]) +} + +// Unit test message received with no session ID +func TestHandleMessageNoSessionID(t *testing.T) { + // Create broker adapter + brokerAdapter := brokeradapter.CreateGateway() + // Create a mock broker + mockBroker := mockbrokerdriver.CreateBroker(brokerAdapter) + // Create mock key value store + keyValueStore := mockkeyvaluedriver.CreateKeyValueStore() + // Create new producer service + producerService := produce.NewService(mockBroker, keyValueStore) + // Create new convert query service + convertQueryService := mockconvertquery.NewService() + // Create new request sender service + requestSenderService := mockrequest.NewService() + // Create new service + service := NewService(mockBroker, producerService, convertQueryService, requestSenderService) + + // Create headers containing a sessionID + headers := make(map[string]interface{}) + mockMessage := brokeradapter.Message{ + Headers: headers, + Body: []byte("test message"), + } + + // Assert that there have not been any messages sent yet + assert.Empty(t, mockBroker.Messages) + + // Send the mock message + service.HandleMessage(&mockMessage) + + // Assert that there was no message published + assert.Empty(t, mockBroker.Messages) +} + +// Unit test receival of message and not being able to parse it +func TestFailToConvertQuery(t *testing.T) { + // Create broker adapter + brokerAdapter := brokeradapter.CreateGateway() + // Create a mock broker + mockBroker := mockbrokerdriver.CreateBroker(brokerAdapter) + // Create mock key value store + keyValueStore := mockkeyvaluedriver.CreateKeyValueStore() + // Create new producer service + producerService := produce.NewService(mockBroker, keyValueStore) + producerService.Start() + // Create new convert query service + convertQueryService := mockconvertquery.NewService() + // Create new request sender service + requestSenderService := mockrequest.NewService() + // Create new service + service := NewService(mockBroker, producerService, convertQueryService, requestSenderService) + + // Create mock session and mock queue + mockSession := "mock-session" + mockQueue := "mock-queue" + + // Set the test-session sessionID queue to mock-queue in key value store + keyValueStore.Set(&mockSession, &mockQueue) + + // Create headers containing a sessionID + headers := make(map[string]interface{}) + headers["sessionID"] = mockSession + mockMessage := brokeradapter.Message{ + Headers: headers, + Body: []byte("test message"), + } + + // Make it so that the conversion service throws an error + convertQueryService.ToggleError() + + // Assert that there have not been any messages sent yet + assert.Empty(t, mockBroker.Messages) + + // Send the mock message + service.HandleMessage(&mockMessage) + + // Assert that there was an error message published + var errorMsg map[string]interface{} + json.Unmarshal(mockBroker.Messages[mockQueue][0].Body, &errorMsg) + assert.Equal(t, "query_translation_error", errorMsg["type"]) +} + +// Test AQL querying error handling +func TestArangoError(t *testing.T) { + // Create broker adapter + brokerAdapter := brokeradapter.CreateGateway() + // Create a mock broker + mockBroker := mockbrokerdriver.CreateBroker(brokerAdapter) + // Create mock key value store + keyValueStore := mockkeyvaluedriver.CreateKeyValueStore() + // Create new producer service + producerService := produce.NewService(mockBroker, keyValueStore) + producerService.Start() + // Create new convert query service + convertQueryService := mockconvertquery.NewService() + // Create new request sender service + requestSenderService := mockrequest.NewService() + // Create new service + service := NewService(mockBroker, producerService, convertQueryService, requestSenderService) + + // Create mock session and mock queue + mockSession := "mock-session" + mockQueue := "mock-queue" + + // Set the test-session sessionID queue to mock-queue in key value store + keyValueStore.Set(&mockSession, &mockQueue) + + // Create headers containing a sessionID + headers := make(map[string]interface{}) + headers["sessionID"] = mockSession + mockMessage := brokeradapter.Message{ + Headers: headers, + Body: []byte("test message"), + } + + // Make it so that the request sender service throws an error + requestSenderService.ToggleError() + + // Assert that there have not been any messages sent yet + assert.Empty(t, mockBroker.Messages) + + // Send the mock message + service.HandleMessage(&mockMessage) + + // Assert that there was an error message published + var errorMsg map[string]interface{} + json.Unmarshal(mockBroker.Messages[mockQueue][1].Body, &errorMsg) + assert.Equal(t, "query_database_error", errorMsg["type"]) +} diff --git a/internal/usecases/consume/handlemessage.go b/internal/usecases/consume/handlemessage.go index c28a24b539800ad65a12b7738ddf1b66c905c20d..95caff83e553221d2a884c1b7d195b31d97e41ed 100644 --- a/internal/usecases/consume/handlemessage.go +++ b/internal/usecases/consume/handlemessage.go @@ -4,7 +4,6 @@ import ( "encoding/json" "query-service/internal/adapters/brokeradapter" "query-service/pkg/errorhandler" - "query-service/pkg/logger" "strings" ) @@ -13,13 +12,17 @@ func (s *Service) HandleMessage(msg *brokeradapter.Message) { // Grab sessionID from the headers sessionID, ok := msg.Headers["sessionID"].(string) if !ok { - // TODO: Handle error where there is no session ID supplied + return } // Convert the json byte msg to a query string query, err := s.queryConverter.ConvertQuery(&msg.Body) if err != nil { - errorhandler.LogError(err, "failed to parse incoming msg to query language") // TODO: send error message to client instead + errorMsg := make(map[string]string) + errorMsg["type"] = "query_translation_error" + errorMsg["value"] = err.Error() + errorMsgBytes, _ := json.Marshal(errorMsg) + s.producer.PublishMessage(&errorMsgBytes, &sessionID) return } @@ -44,8 +47,12 @@ func (s *Service) HandleMessage(msg *brokeradapter.Message) { // convert result to general (node-link (?)) format result, err := s.requestSender.SendAQLQuery(*query) if err != nil { - logger.Log(err.Error()) - return // TODO: Send message in queue notifying of error + errorMsg := make(map[string]string) + errorMsg["type"] = "query_database_error" + errorMsg["value"] = err.Error() + errorMsgBytes, _ := json.Marshal(errorMsg) + s.producer.PublishMessage(&errorMsgBytes, &sessionID) + return } // Add type indicator to result from database diff --git a/internal/usecases/consume/mock/consume.go b/internal/usecases/consume/mock/consume.go new file mode 100644 index 0000000000000000000000000000000000000000..95fb4ff8722a1e85d97a20e8e551b63bdf6b67c1 --- /dev/null +++ b/internal/usecases/consume/mock/consume.go @@ -0,0 +1,18 @@ +package mockconsume + +// A Service implements the consume usecase interface (mock) +type Service struct { + throwError bool +} + +// NewService creates a new consume service (mock) +func NewService() *Service { + return &Service{ + throwError: false, + } +} + +// Start starts the consume service (mock) +func (s *Service) Start() { + +} diff --git a/internal/usecases/convertquery/aql.go b/internal/usecases/convertquery/aql.go index 312a529511e0db8e757b8bd7637982ee48e2a233..f2bf420726fff1085d9f25c16dcb994084a2f7cf 100644 --- a/internal/usecases/convertquery/aql.go +++ b/internal/usecases/convertquery/aql.go @@ -2,7 +2,9 @@ package convertquery import ( "encoding/json" + "errors" "fmt" + "query-service/internal/entity" ) /* @@ -19,6 +21,34 @@ func (s *Service) ConvertQuery(jsonMsg *[]byte) (*string, error) { return nil, err } + // Check to make sure all indexes exist + // How many entities are there + numEntities := len(jsonStruct.Entities) - 1 + // How many relations there are + numRelations := len(jsonStruct.Relations) - 1 + + // Make sure no entity should be returned that is outside the range of that list + for _, e := range jsonStruct.Return.Entities { + // If this entity references an entity that is outside the range + if e > numEntities || e < 0 { + return nil, errors.New("non-existing entity referenced in return") + } + } + + // Make sure that no relation mentions a non-existing entity + for _, r := range jsonStruct.Relations { + if r.EntityFrom > numEntities || r.EntityTo > numEntities { + return nil, errors.New("non-exisiting entity referenced in relation") + } + } + + // Make sure no non-existing relation is tried to be returned + for _, r := range jsonStruct.Return.Relations { + if r > numRelations || r < 0 { + return nil, errors.New("non-existing relation referenced in return") + } + } + result := createQuery(jsonStruct) return result, nil } @@ -28,8 +58,8 @@ Parameters: jsonMsg is the JSON file directly outputted by the drag and drop que Return: parsedJSON is a struct with the same structure and holding the same data as jsonMsg */ -func convertJSONToStruct(jsonMsg *[]byte) (*parsedJSON, error) { - jsonStruct := parsedJSON{} +func convertJSONToStruct(jsonMsg *[]byte) (*entity.QueryParsedJSON, error) { + jsonStruct := entity.QueryParsedJSON{} err := json.Unmarshal(*jsonMsg, &jsonStruct) if err != nil { @@ -44,7 +74,7 @@ Parameters: jsonQuery is a parsedJSON struct holding all the data needed to form Return: a string containing the corresponding AQL query and an error */ -func createQuery(jsonQuery *parsedJSON) *string { +func createQuery(jsonQuery *entity.QueryParsedJSON) *string { // GROTE SIDENOTE: // Vrij zeker dat een query waar alléén edges worden opgevraagd (#4) // niet wordt gesupport door zowel de result parser als de frontend receiver @@ -62,16 +92,18 @@ func createQuery(jsonQuery *parsedJSON) *string { for i, relation := range jsonQuery.Relations { relationName := fmt.Sprintf("r%v", i) - if relation.EntityFrom != -1 { + if relation.EntityFrom >= 0 { // if there is a from-node // create the let for this node fromName := fmt.Sprintf("n%v", relation.EntityFrom) + ret += *createNodeLet(&jsonQuery.Entities[relation.EntityFrom], &fromName) ret += *createRelationLetWithFromEntity(&relation, relationName, &jsonQuery.Entities, jsonQuery.Limit) - } else if relation.EntityTo != -1 { + } else if relation.EntityTo >= 0 { // if there is only a to-node toName := fmt.Sprintf("n%v", relation.EntityTo) + ret += *createNodeLet(&jsonQuery.Entities[relation.EntityTo], &toName) ret += *createRelationLetWithOnlyToEntity(&relation, relationName, &jsonQuery.Entities, jsonQuery.Limit) @@ -135,7 +167,7 @@ name is the autogenerated name of the node consisting of "n" + the index of the Return: a string containing a single LET-statement in AQL */ -func createNodeLet(node *entityStruct, name *string) *string { +func createNodeLet(node *entity.QueryEntityStruct, name *string) *string { header := fmt.Sprintf("LET %v = (\n\tFOR x IN %v \n", *name, node.Type) footer := "\tRETURN x\n)\n" constraints := *createConstraintStatements(&node.Constraints, "x", false) @@ -151,7 +183,7 @@ entities is a list of entityStructs that are needed to form the relation LET-sta Return: a string containing a single LET-statement in AQL */ -func createRelationLetWithFromEntity(relation *relationStruct, name string, entities *[]entityStruct, limit int) *string { +func createRelationLetWithFromEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string { header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityFrom) forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v OUTBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type) @@ -185,7 +217,7 @@ entities is a list of entityStructs that are needed to form the relation LET-sta Return: a string containing a single LET-statement in AQL */ -func createRelationLetWithOnlyToEntity(relation *relationStruct, name string, entities *[]entityStruct, limit int) *string { +func createRelationLetWithOnlyToEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string { header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityTo) forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v INBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type) diff --git a/internal/usecases/convertquery/aqlStructs.go b/internal/usecases/convertquery/aqlStructs.go index c9a042430588cc706503a59d8916476ea1ef26aa..ad1be5eeb27d258675af096fa1596c9711470cf5 100644 --- a/internal/usecases/convertquery/aqlStructs.go +++ b/internal/usecases/convertquery/aqlStructs.go @@ -8,55 +8,3 @@ type Service struct { func NewService() *Service { return &Service{} } - -// Struct used for JSON conversion of the incoming byte array -type parsedJSON struct { - Return returnStruct - Entities []entityStruct - Relations []relationStruct - - // Limit is for limiting the amount of paths AQL will return in a relation let statement - Limit int -} - -// returnStruct holds the indices of the entities and relations that need to be returned -type returnStruct struct { - Entities []int - Relations []int -} - -// entityStruct encapsulates a single entity with its corresponding constraints -type entityStruct struct { - Type string - Constraints []constraintStruct -} - -// relationStruct encapsulates a single relation with its corresponding constraints -type relationStruct struct { - Type string - EntityFrom int - EntityTo int - Depth searchDepthStruct - Constraints []constraintStruct -} - -// searchDepthStruct holds the range of traversals for the relation -type searchDepthStruct struct { - Min int - Max int -} - -/* -constraintStruct holds the information of the constraint - -Constraint datatypes - text MatchTypes: exact/contains/startswith/endswith - number MatchTypes: GT/LT/EQ - bool MatchTypes: EQ/NEQ -*/ -type constraintStruct struct { - Attribute string - Value string - DataType string - MatchType string -} diff --git a/internal/usecases/convertquery/aql_test.go b/internal/usecases/convertquery/aql_test.go index 8df31686eb7b29a92b403d94ddc894294d19af08..13f2c75bdf7025e4e489e51401d390a9ac959fba 100644 --- a/internal/usecases/convertquery/aql_test.go +++ b/internal/usecases/convertquery/aql_test.go @@ -1,249 +1,600 @@ package convertquery import ( + "errors" + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestMock(t *testing.T) { +func TestEmptyQueryConversion(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() - // s := `{"Return":{"Entities":[0,1],"Relations":[0]},"Entities":[{"Type":"airports","Constraints":[{"Attribute":"country","Value":"USA","DataType":"text","MatchType":"exact"}]},{"Type":"airports","Constraints":[{"Attribute":"city","Value":"New York","DataType":"text","MatchType":"exact"},{"Attribute":"vip","Value":"true","DataType":"bool","MatchType":"exact"}]}],"Relations":[{"Type":"flights","Depth":{"min":1,"max":1},"EntityFrom":0,"EntityTo":1,"Constraints":[{"Attribute":"Month","Value":"1","DataType":"number","MatchType":"exact"},{"Attribute":"Day","Value":"15","DataType":"number","MatchType":"exact"}]}]}` + query := []byte(`{ + "return": { + "entities": [], + "relations": [] + }, + "entities": [], + "relations": [], + "limit": 5000 + }`) - // s3 := []byte(s) + convertedResult, err := service.ConvertQuery(&query) - // // Convert the json byte msg to a query string - // convertQueryService := NewService() - // query, err := convertQueryService.ConvertQuery(&s3) - // if err != nil { - // errorhandler.LogError(err, "failed to parse incoming msg to query language") // TODO: send error message to client - // return - // } - // fmt.Println("Query: " + *query) + // Assert that there is no error + assert.NoError(t, err) - // // Make request to database - // // TODO : Generate database seperately - // // execute and retrieve result - // // convert result to general (node-link (?)) format - // requestService := request.NewService() - // result, err := requestService.SendAQLQuery(*query) - // if err != nil { - // logger.Log(err.Error()) - // return // TODO: Send message in queue notifying of error - // } + // Assert that the result and the expected result are the same + correctConvertedResult := ` +LET nodes = first(RETURN UNION_DISTINCT([],[])) +LET edges = first(RETURN UNION_DISTINCT([],[])) +RETURN {"vertices":nodes, "edges":edges }` + assert.Equal(t, correctConvertedResult, *convertedResult) +} + +func TestEntityOneAttributeQuery(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.NoError(t, err) + + // Assert that the result and the expected result are the same + correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET nodes = first(RETURN UNION_DISTINCT(n0,[],[]))LET edges = first(RETURN UNION_DISTINCT([],[]))RETURN {"vertices":nodes, "edges":edges }` + cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") + cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") + assert.Equal(t, correctConvertedResult, cleanedResult) +} - // fmt.Print("QueryResult: ") - // fmt.Println(*result) +func TestRelationWithConstraint(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() - assert.True(t, true, true) + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": 0, + "entityTo": -1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "number", + "matchType": "EQ" + } + ] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.NoError(t, err) + + // Assert that the result and the expected result are the same + correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }` + cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") + cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") + assert.Equal(t, correctConvertedResult, cleanedResult) } -// func TestHugeQuery(t *testing.T) { - -// s := `{ -// "Return": { -// "Entities": [ -// 0, -// 1 -// ], -// "Relations": [ -// 0 -// ] -// }, -// "Entities": [ -// { -// "Type": "airports", -// "Constraints": [ -// { -// "Attribute": "country", -// "Value": "USA", -// "DataType": "text", -// "MatchType": "exact" -// } -// ] -// }, -// { -// "Type": "airports", -// "Constraints": [ -// { -// "Attribute": "city", -// "Value": "New York", -// "DataType": "text", -// "MatchType": "exact" -// }, -// { -// "Attribute": "vip", -// "Value": "true", -// "DataType": "bool", -// "MatchType": "exact" -// } -// ] -// } -// ], -// "Relations": [ -// { -// "Type": "flights", -// "Depth": { -// "min": 1, -// "max": 1 -// }, -// "EntityFrom": 0, -// "EntityTo": 1, -// "Constraints": [ -// { -// "Attribute": "Month", -// "Value": "1", -// "DataType": "number", -// "MatchType": "exact" -// }, -// { -// "Attribute": "Day", -// "Value": "15", -// "DataType": "number", -// "MatchType": "exact" -// } -// ] -// } -// ] -// }` - -// s3 := []byte(s) -// convertQueryService := NewService() -// j, _ := convertQueryService.ConvertQuery(&s3) - -// expected := `LET n0 = ( -// FOR x IN airports -// FILTER x.country == "USA" -// RETURN x -// ) -// LET r0 = ( -// FOR x IN n0 -// FOR v, e, p IN 1..1 OUTBOUND x flights -// OPTIONS { uniqueEdges: "path" } -// FILTER v.city == "New York" -// AND v.vip == true -// FILTER p.edges[*].Month ALL == 1 -// AND p.edges[*].Day ALL == 15 -// LIMIT 1000 -// RETURN DISTINCT p ) - -// LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[])) -// LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[])) -// RETURN {"vertices":nodes, "edges":edges }` - -// assert.Equal(t, *j, expected) -// } -// func TestOnlyEntitiesQuery(t *testing.T) { - -// s := `{ -// "Return": { -// "Entities": [ -// 0 -// ], -// "Relations": [] -// }, -// "Entities": [ -// { -// "Type": "airports", -// "Constraints": [ -// { -// "Attribute": "city", -// "Value": "New York", -// "DataType": "text", -// "MatchType": "exact" -// }, -// { -// "Attribute": "country", -// "Value": "USA", -// "DataType": "text", -// "MatchType": "exact" -// } -// ] -// } -// ], -// "Relations": [] -// }` - -// s3 := []byte(s) -// convertQueryService := NewService() -// j, _ := convertQueryService.ConvertQuery(&s3) - -// expected := `LET n0 = ( -// FOR x IN airports -// FILTER x.city == "New York" -// AND x.country == "USA" -// RETURN x -// ) - -// LET nodes = first(RETURN UNION_DISTINCT(n0,[],[])) -// LET edges = first(RETURN UNION_DISTINCT([],[])) -// RETURN {"vertices":nodes, "edges":edges }` - -// assert.Equal(t, expected, *j) -// } -// func TestInboundQuery(t *testing.T) { - -// s := `{ -// "Return": { -// "Entities": [ -// 0 -// ], -// "Relations": [ -// 0 -// ] -// }, -// "Entities": [ -// { -// "Type": "airports", -// "Constraints": [ -// { -// "Attribute": "city", -// "Value": "New York", -// "DataType": "text", -// "MatchType": "exact" -// } -// ] -// } -// ], -// "Relations": [ -// { -// "Type": "flights", -// "Depth": { -// "min": 1, -// "max": 1 -// }, -// "EntityFrom": -1, -// "EntityTo": 0, -// "Constraints": [{ -// "Attribute": "Day", -// "Value": "15", -// "DataType": "number", -// "MatchType": "exact" -// }] -// } -// ] -// }` - -// s3 := []byte(s) -// convertQueryService := NewService() -// j, _ := convertQueryService.ConvertQuery(&s3) - -// expected := `LET n0 = ( -// FOR x IN airports -// FILTER x.city == "New York" -// RETURN x -// ) -// LET r0 = ( -// FOR x IN n0 -// FOR v, e, p IN 1..1 INBOUND x flights -// OPTIONS { uniqueEdges: "path" } -// FILTER p.edges[*].Day ALL == 15 -// LIMIT 1000 -// RETURN DISTINCT p ) - -// LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[])) -// LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[])) -// RETURN {"vertices":nodes, "edges":edges }` - -// assert.Equal(t, expected, *j) -// } +func TestRelationWithInOutConstraint(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0, + 1 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 3 + }, + "entityFrom": 1, + "entityTo": 0, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "number", + "matchType": "EQ" + } + ] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.NoError(t, err) + + // Assert that the result and the expected result are the same + correctConvertedResult := `LET n1 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n1 FOR v, e, p IN 1..3 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER v.city == "San Francisco" FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }` + cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") + cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") + assert.Equal(t, correctConvertedResult, cleanedResult) +} + +func TestTwoRelations(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0, + 1, + 2 + ], + "relations": [ + 0, + 1 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "New York", + "dataType": "text", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 3 + }, + "entityFrom": 2, + "entityTo": 1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "number", + "matchType": "EQ" + } + ] + }, + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": 0, + "entityTo": -1, + "constraints": [] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.NoError(t, err) + + // Assert that the result and the expected result are the same + correctConvertedResult := `LET n2 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n2 FOR v, e, p IN 1..3 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER v.city == "San Francisco" FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET n0 = (FOR x IN airports FILTER x.city == "New York" RETURN x)LET r1 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), flatten(r1[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), flatten(r1[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }` + cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") + cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") + assert.Equal(t, correctConvertedResult, cleanedResult) +} + +func TestRelationWithOnlyToNode(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -1, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.NoError(t, err) + + // Assert that the result and the expected result are the same + correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.city == "San Francisco" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 INBOUND x flights OPTIONS { uniqueEdges: "path" }LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }` + cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") + cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") + assert.Equal(t, correctConvertedResult, cleanedResult) +} + +func TestTooManyReturnEntities(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0, + 1, + 2 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -1, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + _, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.Equal(t, errors.New("non-existing entity referenced in return"), err) +} + +func TestTooManyReturnRelations(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [ + 0, + 1, + 2 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -1, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + _, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.Equal(t, errors.New("non-existing relation referenced in return"), err) +} + +func TestNegativeReturnEntities(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0, + -1 + ], + "relations": [ + 0, + 1, + 2 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -1, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + _, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.Equal(t, errors.New("non-existing entity referenced in return"), err) +} + +func TestNoRelationsField(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "limit": 5000 + }`) + + convertedResult, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.NoError(t, err) + + // Assert that the result and the expected result are the same + correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.city == "San Francisco" RETURN x)LET nodes = first(RETURN UNION_DISTINCT(n0,[],[]))LET edges = first(RETURN UNION_DISTINCT([],[]))RETURN {"vertices":nodes, "edges":edges }` + cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "") + cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "") + assert.Equal(t, correctConvertedResult, cleanedResult) +} + +func TestEntityFromLowerThanNegativeOneInRelation(t *testing.T) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [ + 0 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": -4, + "entityTo": 0, + "constraints": [] + } + ], + "limit": 5000 + }`) + + _, err := service.ConvertQuery(&query) + + // Assert that there is no error + assert.NoError(t, err) +} diff --git a/internal/usecases/convertquery/benchmark_test.go b/internal/usecases/convertquery/benchmark_test.go new file mode 100644 index 0000000000000000000000000000000000000000..86da73b02be46cef4d15b4f7ec7bd5d75ac74de0 --- /dev/null +++ b/internal/usecases/convertquery/benchmark_test.go @@ -0,0 +1,152 @@ +package convertquery + +import "testing" + +func BenchmarkConvertEmptyQuery(b *testing.B) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [], + "relations": [] + }, + "entities": [], + "relations": [], + "limit": 5000 + }`) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + service.ConvertQuery(&query) + } +} + +func BenchmarkConvertOneAttributeQuery(b *testing.B) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0 + ], + "relations": [] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [], + "limit": 5000 + }`) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + service.ConvertQuery(&query) + } +} + +func BenchmarkConvertTwoRelationQuery(b *testing.B) { + // Setup for test + // Create query conversion service + service := NewService() + + query := []byte(`{ + "return": { + "entities": [ + 0, + 1, + 2 + ], + "relations": [ + 0, + 1 + ] + }, + "entities": [ + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "New York", + "dataType": "text", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "city", + "value": "San Francisco", + "dataType": "text", + "matchType": "exact" + } + ] + }, + { + "type": "airports", + "constraints": [ + { + "attribute": "state", + "value": "HI", + "dataType": "text", + "matchType": "exact" + } + ] + } + ], + "relations": [ + { + "type": "flights", + "depth": { + "min": 1, + "max": 3 + }, + "entityFrom": 2, + "entityTo": 1, + "constraints": [ + { + "attribute": "Day", + "value": "15", + "dataType": "number", + "matchType": "EQ" + } + ] + }, + { + "type": "flights", + "depth": { + "min": 1, + "max": 1 + }, + "entityFrom": 0, + "entityTo": -1, + "constraints": [] + } + ], + "limit": 5000 + }`) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + service.ConvertQuery(&query) + } +} diff --git a/internal/usecases/convertquery/createConstraints.go b/internal/usecases/convertquery/createConstraints.go index 2f92a5cb8adf20a9648c27d6404f7eb219a8b80a..b03d06f79342d39122612dd076eeedf5afe37f8b 100644 --- a/internal/usecases/convertquery/createConstraints.go +++ b/internal/usecases/convertquery/createConstraints.go @@ -1,6 +1,9 @@ package convertquery -import "fmt" +import ( + "fmt" + "query-service/internal/entity" +) /* createConstraintStatements generates the appropriate amount of constraint lines calling createConstraingBoolExpression Parameters: constraints is a list of constraintStructs that specify the constraints of a node or relation, @@ -9,7 +12,7 @@ isRelation is a boolean specifying if this constraint comes from a node or relat Return: a string containing a FILTER-statement with all the constraints */ -func createConstraintStatements(constraints *[]constraintStruct, name string, isRelation bool) *string { +func createConstraintStatements(constraints *[]entity.QueryConstraintStruct, name string, isRelation bool) *string { s := "" if len(*constraints) == 0 { return &s @@ -34,7 +37,7 @@ isRelation is a boolean specifying if this constraint comes from a node or relat Return: a string containing an boolean expression of a single constraint */ -func createConstraintBoolExpression(constraint *constraintStruct, name string, isRelation bool) *string { +func createConstraintBoolExpression(constraint *entity.QueryConstraintStruct, name string, isRelation bool) *string { var ( match string value string diff --git a/internal/usecases/convertquery/mock/mockconvertquery.go b/internal/usecases/convertquery/mock/mockconvertquery.go new file mode 100644 index 0000000000000000000000000000000000000000..f30767469f7a592d114bca3bebe69b32d61c8ebc --- /dev/null +++ b/internal/usecases/convertquery/mock/mockconvertquery.go @@ -0,0 +1,30 @@ +package mockconvertquery + +import "errors" + +// A Service implements the query convert usecase interface (mock) +type Service struct { + throwError bool +} + +// NewService creates a new query convert service (mock) +func NewService() *Service { + return &Service{ + throwError: false, + } +} + +// ConvertQuery returns a hard coded string message (mock) +func (s *Service) ConvertQuery(jsonMsg *[]byte) (*string, error) { + mockQuery := "Query converted" + + if !s.throwError { + return &mockQuery, nil + } + return nil, errors.New("Failed to convert query") +} + +// ToggleError decides whether the convert function throws an error +func (s *Service) ToggleError() { + s.throwError = !s.throwError +} diff --git a/internal/usecases/produce/produce.go b/internal/usecases/produce/produce.go index 2bb3ef3efc0ed939ddbdf7d82137c21b3e27c84f..85bff2a1c15774a55a05982025aaeb4bc907f508 100644 --- a/internal/usecases/produce/produce.go +++ b/internal/usecases/produce/produce.go @@ -1,22 +1,19 @@ package produce import ( - "log" "query-service/internal/drivers/brokerdriver" "query-service/internal/drivers/keyvaluedriver" - - "github.com/streadway/amqp" ) // Service wraps consumer methods type Service struct { brokerDriver brokerdriver.Broker - keyValueStore keyvaluedriver.KeyValueStore + keyValueStore keyvaluedriver.KeyValueStoreInterface producerDriver brokerdriver.Producer } // NewService creates a new service -func NewService(broker brokerdriver.Broker, keyValueStore keyvaluedriver.KeyValueStore) *Service { +func NewService(broker brokerdriver.Broker, keyValueStore keyvaluedriver.KeyValueStoreInterface) *Service { return &Service{ brokerDriver: broker, keyValueStore: keyValueStore, @@ -29,19 +26,4 @@ func (s *Service) Start() { p := s.brokerDriver.CreateProducer() s.producerDriver = p - - // // Start consuming messages - // p.ConsumeMessages() -} - -// PublishMessage will publish the message to the queue retrieved from the key value store, with the given sessionID -func (s *Service) PublishMessage(data *[]byte, sessionID *string) { - clientUpdaterID := s.keyValueStore.Get(sessionID) - - log.Println(string(*data)) - - headers := amqp.Table{} - headers["sessionID"] = *sessionID - headers["type"] = "queryResult" - s.producerDriver.PublishMessage(data, clientUpdaterID, &headers) } diff --git a/internal/usecases/produce/produce_test.go b/internal/usecases/produce/produce_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ff3f5fefde093ed8b0ba1c39be4c28444ac3b1ae --- /dev/null +++ b/internal/usecases/produce/produce_test.go @@ -0,0 +1,77 @@ +package produce + +import ( + "query-service/internal/adapters/brokeradapter" + mockbrokerdriver "query-service/internal/drivers/brokerdriver/mock" + mockkeyvaluedriver "query-service/internal/drivers/keyvaluedriver/mock" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Make sure a correct message gets published +func TestPublishCorrectMessage(t *testing.T) { + // Create broker adapter + brokerAdapter := brokeradapter.CreateGateway() + // Create a mock broker + mockBroker := mockbrokerdriver.CreateBroker(brokerAdapter) + // Create mock key value store + keyValueStore := mockkeyvaluedriver.CreateKeyValueStore() + // Create new service and start it + service := NewService(mockBroker, keyValueStore) + service.Start() + + // Create mock session and mock queue + mockSession := "mock-session" + mockQueue := "mock-queue" + + // Set the test-session sessionID queue to mock-queue in key value store + keyValueStore.Set(&mockSession, &mockQueue) + + // Create headers containing a sessionID + headers := make(map[string]interface{}) + headers["sessionID"] = mockSession + + // Assert that there have not been any messages sent yet + assert.Empty(t, mockBroker.Messages) + + // Publish the message + mockMessage := []byte("Test message") + service.PublishMessage(&mockMessage, &mockSession) + + // Assert that there is now 1 message + assert.Len(t, mockBroker.Messages[mockQueue], 1) + + // Assert that this message contains the mockMessage + assert.Equal(t, mockBroker.Messages[mockQueue][0].Body, mockMessage) +} + +// Test publishing message without setting routing in key value store +func TestPublishMessageNoRouting(t *testing.T) { + // Create broker adapter + brokerAdapter := brokeradapter.CreateGateway() + // Create a mock broker + mockBroker := mockbrokerdriver.CreateBroker(brokerAdapter) + // Create mock key value store + keyValueStore := mockkeyvaluedriver.CreateKeyValueStore() + // Create new service and start it + service := NewService(mockBroker, keyValueStore) + service.Start() + + // Create mock session and mock queue + mockSession := "mock-session" + + // Create headers containing a sessionID + headers := make(map[string]interface{}) + headers["sessionID"] = mockSession + + // Assert that there have not been any messages sent yet + assert.Empty(t, mockBroker.Messages) + + // Publish the message + mockMessage := []byte("Test message") + service.PublishMessage(&mockMessage, &mockSession) + + // Assert that there are still 0 messages + assert.Empty(t, mockBroker.Messages) +} diff --git a/internal/usecases/produce/publishmessage.go b/internal/usecases/produce/publishmessage.go new file mode 100644 index 0000000000000000000000000000000000000000..8e9b31e8cef943c1f75df320bce91b1c531a5a98 --- /dev/null +++ b/internal/usecases/produce/publishmessage.go @@ -0,0 +1,22 @@ +package produce + +import ( + "github.com/streadway/amqp" +) + +// PublishMessage will publish the message to the queue retrieved from the key value store, with the given sessionID +func (s *Service) PublishMessage(data *[]byte, sessionID *string) { + // Use the sessionID to query the key value store to get the queue we need to send this message to + clientQueueID := s.keyValueStore.Get(sessionID) + + // If this client has now disconnected + if clientQueueID == "" { + // TODO: Decide whether to throw away the message or perhaps cache it, for now throw it away + return + } + + headers := amqp.Table{} + headers["sessionID"] = *sessionID + headers["type"] = "queryResult" + s.producerDriver.PublishMessage(data, &clientQueueID, &headers) +} diff --git a/internal/usecases/request/interface.go b/internal/usecases/request/interface.go index 7a30fb2ece5001330752ae924eabad893fc37f2a..c54ca39d920bbcdc71452d14138e2603ed86a12b 100644 --- a/internal/usecases/request/interface.go +++ b/internal/usecases/request/interface.go @@ -1,6 +1,8 @@ package request +import "query-service/internal/entity" + // UseCase is an interface describing the request usecases type UseCase interface { - SendAQLQuery(query string) (*map[string][]Document, error) + SendAQLQuery(query string) (*map[string][]entity.Document, error) } diff --git a/internal/usecases/request/mock/mockrequest.go b/internal/usecases/request/mock/mockrequest.go new file mode 100644 index 0000000000000000000000000000000000000000..758a0eaa4b83ba433efc97c472b64b911feda519 --- /dev/null +++ b/internal/usecases/request/mock/mockrequest.go @@ -0,0 +1,33 @@ +package mockrequest + +import ( + "errors" + "query-service/internal/entity" +) + +// A Service implements the request usecases (mock) +type Service struct { + throwError bool +} + +// NewService creates a new service (mock) +func NewService() *Service { + return &Service{ + throwError: false, + } +} + +// SendAQLQuery sends the query to arangoDB and parses the result (mock) +func (s *Service) SendAQLQuery(query string) (*map[string][]entity.Document, error) { + mockResult := make(map[string][]entity.Document) + + if !s.throwError { + return &mockResult, nil + } + return nil, errors.New("Database error") +} + +// ToggleError decides whether the convert function throws an error +func (s *Service) ToggleError() { + s.throwError = !s.throwError +} diff --git a/internal/usecases/request/request.go b/internal/usecases/request/request.go index 083150bda2981ddc309509d50ca2a143e50c0ee5..bfcf2b4368897c94d64035a0cb12eb97422ef92c 100644 --- a/internal/usecases/request/request.go +++ b/internal/usecases/request/request.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "log" "os" + "query-service/internal/entity" "encoding/json" "io/ioutil" @@ -25,10 +26,10 @@ Parameters: AQLQuery is a string containing the query that will be send to the d Return: a map with two entries: "nodes" with a list of vertices/nodes and "edges" with a list of edges that will be returned to the frontend */ -func (s *Service) SendAQLQuery(AQLQuery string) (*map[string][]Document, error) { +func (s *Service) SendAQLQuery(AQLQuery string) (*map[string][]entity.Document, error) { // Get ArangoDB url from environment variable arangoURL := os.Getenv("ARANGO_HOST") - var queryResult = make(map[string][]Document) + var queryResult = make(map[string][]entity.Document) conn, err := http.NewConnection(http.ConnectionConfig{ Endpoints: []string{arangoURL}, TLSConfig: &tls.Config{InsecureSkipVerify: true}, @@ -62,7 +63,7 @@ func (s *Service) SendAQLQuery(AQLQuery string) (*map[string][]Document, error) defer cursor.Close() //Loop through the resulting documents - listContainer := ListContainer{} + listContainer := entity.ListContainer{} for { var doc map[string][]interface{} _, err := cursor.ReadDocument(ctx, &doc) @@ -76,8 +77,8 @@ func (s *Service) SendAQLQuery(AQLQuery string) (*map[string][]Document, error) parseResult(doc, &listContainer) } - queryResult["nodes"] = listContainer.nodeList - queryResult["edges"] = listContainer.edgeList + queryResult["nodes"] = listContainer.NodeList + queryResult["edges"] = listContainer.EdgeList //writeJSON(queryResult) //file, err := json.MarshalIndent(queryResult, "", " ") @@ -91,20 +92,20 @@ listContainer is a struct containing the nodelist and edgelist that will be retu Return: Nothing because the result is stored in the listContainer */ -func parseResult(doc map[string][]interface{}, listContainer *ListContainer) { +func parseResult(doc map[string][]interface{}, listContainer *entity.ListContainer) { vertices := doc["vertices"] edges := doc["edges"] for _, vertex := range vertices { vertexDoc := vertex.(map[string]interface{}) - (*listContainer).nodeList = append((*listContainer).nodeList, parseNode(vertexDoc)) + (*listContainer).NodeList = append((*listContainer).NodeList, parseNode(vertexDoc)) } for _, edge := range edges { edgeDoc := edge.(map[string]interface{}) - (*listContainer).edgeList = append((*listContainer).edgeList, parseEdge(edgeDoc)) + (*listContainer).EdgeList = append((*listContainer).EdgeList, parseEdge(edgeDoc)) } } @@ -113,10 +114,10 @@ Parameters: d is a single entry of an edge Return: a document with almost the same structure as before, but the attributes are grouped */ -func parseEdge(d map[string]interface{}) Document { +func parseEdge(d map[string]interface{}) entity.Document { doc := d //.(map[string]interface{}) - data := make(Document) + data := make(entity.Document) data["_id"] = doc["_id"] delete(doc, "_id") data["_key"] = doc["_key"] @@ -138,7 +139,7 @@ func parseEdge(d map[string]interface{}) Document { } // writeJSON writes a json file for testing purposes -func writeJSON(queryResult map[string][]Document) { +func writeJSON(queryResult map[string][]entity.Document) { file, _ := json.MarshalIndent(queryResult, "", " ") _ = ioutil.WriteFile("result.json", file, 0644) @@ -149,10 +150,10 @@ Parameters: d is a single entry of an node Return: a document with almost the same structure as before, but the attributes are grouped */ -func parseNode(d map[string]interface{}) Document { +func parseNode(d map[string]interface{}) entity.Document { doc := d //.(map[string]interface{}) - data := make(Document) + data := make(entity.Document) data["_id"] = doc["_id"] delete(doc, "_id") data["_key"] = doc["_key"] diff --git a/internal/usecases/request/requestStructs.go b/internal/usecases/request/requestStructs.go index 2479df8200a8ecefd505e6f300963d21d65be28a..b2a001fccd12cb691af698e826bb555d68f2b660 100644 --- a/internal/usecases/request/requestStructs.go +++ b/internal/usecases/request/requestStructs.go @@ -8,15 +8,3 @@ type Service struct { func NewService() *Service { return &Service{} } - -// Document with Empty struct to retrieve all data from the DB Document -type Document map[string]interface{} - -// GeneralFormat with Empty struct to retrieve all data from the DB Document -type GeneralFormat map[string][]Document - -// ListContainer is a struct that keeps track of the nodes and edges that need to be returned -type ListContainer struct { - nodeList []Document - edgeList []Document -}