I'll often approach software from the top down, but often from the bottom up.
I'm sure there's a lot of interestingly psychology behind this, but I think the simple answer is that it tells you where my focus is. On this occasion my focus is very much on the technology - and particularly on Neptune - and writing an application at all is not very interesting.
But I still think in terms of the application, sketch all of the code from the browser downwards in my head, until I figure out what the eventual method/lambda call would be. So right now I am thinking about a user, who, having logged in (i.e. we have their Username) wants to see the current prices of all the stocks they are watching.
In order to do this, we need to follow all the Watching relationships from the given user, then go back and look up the keys in Dynamo. It is obviously the first of these that is more interesting, but let's do both.
Going back to the cheat sheet, we can look at the code to match a relationship; I think the closest thing they have to what I want is:
MATCH (:Movie {title: 'Wall Street'})<-[:ACTED_IN]-(actor:Person)So I want something like this:
RETURN actor.name AS actor
MATCH (:User {username: "user008"})-[:watching]->{s:Stock}I think I can try this using curl and debug it there:
RETURN stock.symbol AS symbol
$ curl -k https://user-stocks.cluster-ckgvna81hufy.us-east-1.neptune.amazonaws.com:8182/openCypher -d 'query=MATCH (u:User {username:$username})-[r]->(s:Stock) RETURN u.username, s.symbol' -d 'parameters={"username": "user003"}'So now let's try to put that in code. We need a new main program to drive this, which I'll call stockprices:
{
"results": [{
"u.username": "user003",
"s.symbol": "CWT5"
}, {
"u.username": "user003",
"s.symbol": "AUOR"
}, {
"u.username": "user003",
"s.symbol": "DRYM"
}, {
"u.username": "user003",
"s.symbol": "PMA2"
}, {
"u.username": "user003",
"s.symbol": "UDBO"
}, {
"u.username": "user003",
"s.symbol": "GKXW"
}, {
"u.username": "user003",
"s.symbol": "RTO2"
}, {
"u.username": "user003",
"s.symbol": "IFC0"
}, {
"u.username": "user003",
"s.symbol": "PFW9"
}, {
"u.username": "user003",
"s.symbol": "POCG"
}, {
"u.username": "user003",
"s.symbol": "ZKDI"
}, {
"u.username": "user003",
"s.symbol": "IRI8"
}, {
"u.username": "user003",
"s.symbol": "XEH1"
}, {
"u.username": "user003",
"s.symbol": "UBD9"
}, {
"u.username": "user003",
"s.symbol": "MAGS"
}, {
"u.username": "user003",
"s.symbol": "OGCY"
}, {
"u.username": "user003",
"s.symbol": "WUIU"
}, {
"u.username": "user003",
"s.symbol": "DJBS"
}, {
"u.username": "user003",
"s.symbol": "UPM6"
}, {
"u.username": "user003",
"s.symbol": "ULB4"
}, {
"u.username": "user003",
"s.symbol": "EUT6"
}, {
"u.username": "user003",
"s.symbol": "LNRF"
}]
}
package main
import (
"fmt"
"log"
"os"
"slices"
"github.com/gmmapowell/ignorance/neptune/internal/dynamo"
"github.com/gmmapowell/ignorance/neptune/internal/neptune"
)
func main() {
if len(os.Args) < 2 {
log.Printf("Usage: stockprices <user>")
return
}
user := os.Args[1]
stocks, err := neptune.FindWatchedStocks("user-stocks", user)
if err != nil {
panic(err)
}
if len(stocks) == 0 {
fmt.Printf("no stocks found\n")
return
}
slices.Sort(stocks)
prices, err := dynamo.FindStockPrices("Stocks", stocks)
if err != nil {
panic(err)
}
for _, s := range stocks {
fmt.Printf("%s: %d\n", s, prices[s])
}
}
NEPTUNE_STOCK_PRICES:neptune/cmd/stockprices/main.go
This calls the neptune engine to find all the stocks associated with the user specified in argument 1, and then goes to dynamo to find the prices of all these stocks. It then sorts the stocks by symbol and prints them out.Turning to the dynamo code, we have the query from above embedded into a new function FindWatchedStocks:
package neptune
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/neptunedata"
)
func FindWatchedStocks(db string, user string) ([]string, error) {
svc, err := openNeptune(db)
if err != nil {
return nil, err
}
query := `
MATCH (u:User {username:$username})-[r]->(s:Stock)
RETURN u.username, s.symbol
`
params := fmt.Sprintf(`{"username": "%s"}`, user)
linkQuery := neptunedata.ExecuteOpenCypherQueryInput{OpenCypherQuery: aws.String(query), Parameters: aws.String(params)}
out, err := svc.ExecuteOpenCypherQuery(context.TODO(), &linkQuery)
if err != nil {
return nil, err
}
results, err := unpack(out.Results)
if err != nil {
return nil, err
}
var ret []string
for _, m := range results {
ret = append(ret, m["s.symbol"].(string))
}
return ret, nil
}
NEPTUNE_STOCK_PRICES:neptune/internal/neptune/findStocks.go
I'm not really sure what to say about this, except it is an embedding in Go of the query (with parameters) I showed above. We then go through all the results and extract the fields s.symbol and build a list. The user is the same in all the entries because we specified that in the query parameters.We then pass this list to the FindPrices function in the dynamo package:
package dynamo
import (
"context"
"strconv"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func FindStockPrices(table string, stocks []string) (map[string]int, error) {
svc, err := openDynamo()
if err != nil {
return nil, err
}
var keys []map[string]types.AttributeValue
var attrs []string
for _, s := range stocks {
key := make(map[string]types.AttributeValue)
key["Symbol"] = &types.AttributeValueMemberS{Value: s}
keys = append(keys, key)
}
attrs = append(attrs, "Symbol", "Price")
tableRequest := make(map[string]types.KeysAndAttributes)
tableRequest[table] = types.KeysAndAttributes{Keys: keys, AttributesToGet: attrs}
out, err := svc.BatchGetItem(context.TODO(), &dynamodb.BatchGetItemInput{RequestItems: tableRequest})
if err != nil {
return nil, err
}
ret := make(map[string]int)
for _, x := range out.Responses[table] {
sym := x["Symbol"].(*types.AttributeValueMemberS).Value
price := x["Price"].(*types.AttributeValueMemberN).Value
ret[sym], err = strconv.Atoi(price)
}
return ret, nil
}
NEPTUNE_STOCK_PRICES:neptune/internal/dynamo/findPrices.go
This looks complicated, but that's only because the Dynamo API is so awful. For efficiency, we want to fetch all of the stock prices together, which involves a BatchGetItem call. This obviously requires multiple keys, but instead of just taking a list, it insists on having it carefully constructed. I'm sure there's a reason that applies to cases I have never considered.Separately, it wants a list of attributes that you want to recover. For some reason, it doesn't (as far as I can see) give you the key back, so you have to say that you want the key fields (Symbol) as well as the attribute we want (Price).
And then we receive back a list of Responses (which I think of as Rows, but are more properly Documents). Each of these has an entry for each of the attributes requested, so we need to extract the Symbol and the Price and then we can build a map.
And when we've finished, we get this:
$ cmd/stockprices/stockprices user003
AUOR: 313
CWT5: 189
DJBS: 456
DRYM: 150
EUT6: 181
GKXW: 329
IFC0: 355
IRI8: 478
LNRF: 322
MAGS: 371
OGCY: 434
PFW9: 415
PMA2: 190
POCG: 477
RTO2: 428
UBD9: 424
UDBO: 180
ULB4: 105
UPM6: 239
WUIU: 257
XEH1: 173
ZKDI: 183
No comments:
Post a Comment