super-graph/serv/core.go

283 lines
5.1 KiB
Go
Raw Normal View History

2019-04-19 07:55:03 +02:00
package serv
import (
2019-05-13 01:27:26 +02:00
"bytes"
2019-04-19 07:55:03 +02:00
"context"
"encoding/json"
"fmt"
"io"
2019-05-13 01:27:26 +02:00
"net/http"
2019-04-19 07:55:03 +02:00
"strings"
"time"
2019-05-13 01:27:26 +02:00
"github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/jsn"
2019-04-20 06:35:57 +02:00
"github.com/dosco/super-graph/qcode"
2019-04-19 07:55:03 +02:00
"github.com/go-pg/pg"
"github.com/valyala/fasttemplate"
)
2019-05-13 01:27:26 +02:00
const (
empty = ""
2019-04-20 06:35:57 +02:00
)
2019-05-13 01:27:26 +02:00
// var (
// cache, _ = bigcache.NewBigCache(bigcache.DefaultConfig(24 * time.Hour))
// )
2019-04-19 07:55:03 +02:00
2019-05-13 01:27:26 +02:00
type coreContext struct {
req gqlReq
res gqlResp
context.Context
}
func (c *coreContext) handleReq(w io.Writer, req *http.Request) error {
2019-04-20 06:35:57 +02:00
var err error
2019-04-19 07:55:03 +02:00
2019-05-13 01:27:26 +02:00
//cacheEnabled := (conf.EnableTracing == false)
2019-04-20 06:35:57 +02:00
2019-05-13 01:27:26 +02:00
qc, err := qcompile.CompileQuery(c.req.Query)
if err != nil {
return err
2019-04-19 07:55:03 +02:00
}
2019-05-13 01:27:26 +02:00
vars := varMap(c)
data, skipped, err := c.resolveSQL(qc, vars)
if err != nil {
return err
}
2019-04-19 07:55:03 +02:00
2019-05-13 01:27:26 +02:00
if len(data) == 0 || skipped == 0 {
return c.render(w, data)
}
2019-04-19 07:55:03 +02:00
2019-05-13 01:27:26 +02:00
sel := qc.Query.Selects
h := xxhash.New()
// fetch the field name used within the db response json
// that are used to mark insertion points and the mapping between
// those field names and their select objects
fids, sfmap := parentFieldIds(h, sel, skipped)
// fetch the field values of the marked insertion points
// these values contain the id to be used with fetching remote data
from := jsn.Get(data, fids)
// replacement data for the marked insertion points
// key and value will be replaced by whats below
to := make([]jsn.Field, 0, len(from))
for _, id := range from {
// use the json key to find the related Select object
k1 := xxhash.Sum64(id.Key)
s, ok := sfmap[k1]
if !ok {
continue
2019-04-20 06:35:57 +02:00
}
2019-05-13 01:27:26 +02:00
p := sel[s.ParentID]
2019-04-19 07:55:03 +02:00
2019-05-13 01:27:26 +02:00
// then use the Table nme in the Select and it's parent
// to find the resolver to use for this relationship
k2 := mkkey(h, s.Table, p.Table)
2019-04-20 06:35:57 +02:00
2019-05-13 01:27:26 +02:00
r, ok := rmap[k2]
if !ok {
continue
}
2019-04-20 06:35:57 +02:00
2019-05-13 01:27:26 +02:00
id := jsn.Value(id.Value)
if len(id) == 0 {
continue
2019-04-20 06:35:57 +02:00
}
2019-05-13 01:27:26 +02:00
b, err := r.Fn(req, id)
2019-04-20 06:35:57 +02:00
if err != nil {
return err
}
2019-05-13 01:27:26 +02:00
if len(r.Path) != 0 {
b = jsn.Strip(b, r.Path)
}
2019-04-20 06:35:57 +02:00
2019-05-13 01:27:26 +02:00
fils := []string{}
for i := range s.Cols {
fils = append(fils, s.Cols[i].Name)
}
var ob bytes.Buffer
if err = jsn.Filter(&ob, b, fils); err != nil {
return err
}
f := jsn.Field{[]byte(s.FieldName), ob.Bytes()}
to = append(to, f)
}
var ob bytes.Buffer
err = jsn.Replace(&ob, data, from, to)
if err != nil {
2019-04-19 07:55:03 +02:00
return err
2019-05-13 01:27:26 +02:00
}
// if cacheEnabled {
// if err = cache.Set(key, []byte(finalSQL)); err != nil {
// return err
// }
// }
return c.render(w, ob.Bytes())
}
func (c *coreContext) resolveSQL(qc *qcode.QCode, vars variables) (
[]byte, uint32, error) {
//var entry []byte
//var key string
//cacheEnabled := (conf.EnableTracing == false)
// if cacheEnabled {
// k := sha1.Sum([]byte(req.Query))
// key = string(k[:])
// entry, err = cache.Get(key)
// if err != nil && err != bigcache.ErrEntryNotFound {
// return emtpy, err
// }
// if len(entry) != 0 && err == nil {
// return entry, nil
// }
// }
2019-04-20 06:35:57 +02:00
2019-05-13 01:27:26 +02:00
skipped, stmts, err := pcompile.Compile(qc)
if err != nil {
return nil, 0, err
}
t := fasttemplate.New(stmts[0], openVar, closeVar)
var sqlStmt strings.Builder
_, err = t.Execute(&sqlStmt, vars)
if err == errNoUserID &&
authFailBlock == authFailBlockPerQuery &&
authCheck(c) == false {
return nil, 0, errUnauthorized
2019-04-19 07:55:03 +02:00
}
2019-05-13 01:27:26 +02:00
if err != nil {
return nil, 0, err
}
finalSQL := sqlStmt.String()
2019-04-19 07:55:03 +02:00
if conf.DebugLevel > 0 {
fmt.Println(finalSQL)
}
2019-05-13 01:27:26 +02:00
2019-04-19 07:55:03 +02:00
st := time.Now()
var root json.RawMessage
_, err = db.Query(pg.Scan(&root), finalSQL)
if err != nil {
2019-05-13 01:27:26 +02:00
return nil, 0, err
2019-04-19 07:55:03 +02:00
}
2019-05-13 01:27:26 +02:00
if conf.EnableTracing {
c.res.Extensions = &extensions{newTrace(st, time.Now(), qc)}
}
2019-04-19 07:55:03 +02:00
2019-05-13 01:27:26 +02:00
return []byte(root), skipped, nil
}
func (c *coreContext) render(w io.Writer, data []byte) error {
c.res.Data = json.RawMessage(data)
return json.NewEncoder(w).Encode(c.res)
}
func parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) (
[][]byte,
map[uint64]*qcode.Select) {
c := 0
for i := range sel {
s := &sel[i]
if isSkipped(skipped, s.ID) {
c++
2019-04-20 06:35:57 +02:00
}
}
2019-05-13 01:27:26 +02:00
// list of keys (and it's related value) to extract from
// the db json response
fm := make([][]byte, c)
// mapping between the above extracted key and a Select
// object
sm := make(map[uint64]*qcode.Select, c)
n := 0
for i := range sel {
s := &sel[i]
if isSkipped(skipped, s.ID) == false {
continue
}
p := sel[s.ParentID]
k := mkkey(h, s.Table, p.Table)
if r, ok := rmap[k]; ok {
fm[n] = r.IDField
n++
k := xxhash.Sum64(r.IDField)
sm[k] = s
}
}
return fm, sm
}
func isSkipped(n uint32, pos uint16) bool {
return ((n & (1 << pos)) != 0)
}
func authCheck(ctx *coreContext) bool {
return (ctx.Value(userIDKey) != nil)
}
func newTrace(st, et time.Time, qc *qcode.QCode) *trace {
if len(qc.Query.Selects) == 0 {
return nil
}
du := et.Sub(et)
sel := qc.Query.Selects[0]
t := &trace{
Version: 1,
StartTime: st,
EndTime: et,
Duration: du,
Execution: execution{
[]resolver{
resolver{
Path: []string{sel.Table},
ParentType: "Query",
FieldName: sel.Table,
ReturnType: "object",
StartOffset: 1,
Duration: du,
},
},
},
2019-04-19 07:55:03 +02:00
}
2019-05-13 01:27:26 +02:00
return t
2019-04-19 07:55:03 +02:00
}