super-graph/serv/core.go

579 lines
10 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"
2019-06-02 01:48:42 +02:00
"errors"
2019-06-04 16:54:51 +02:00
"fmt"
2019-04-19 07:55:03 +02:00
"io"
2019-05-13 01:27:26 +02:00
"net/http"
"sync"
2019-04-19 07:55:03 +02:00
"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"
"github.com/jackc/pgx/v4"
2019-04-19 07:55:03 +02:00
"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
type coreContext struct {
req gqlReq
res gqlResp
context.Context
}
func (c *coreContext) handleReq(w io.Writer, req *http.Request) error {
2019-09-20 06:19:11 +02:00
c.req.ref = req.Referer()
c.req.hdr = req.Header
2019-10-15 08:30:19 +02:00
if authCheck(c) {
c.req.role = "user"
2019-10-15 08:30:19 +02:00
} else {
c.req.role = "anon"
2019-10-15 08:30:19 +02:00
}
b, err := c.execQuery()
2019-09-20 06:19:11 +02:00
if err != nil {
return err
}
return c.render(w, b)
}
func (c *coreContext) execQuery() ([]byte, error) {
2019-04-20 06:35:57 +02:00
var err error
2019-07-29 07:13:33 +02:00
var skipped uint32
var qc *qcode.QCode
var data []byte
2019-04-19 07:55:03 +02:00
2019-11-07 08:37:24 +01:00
if conf.Production {
2019-07-29 07:13:33 +02:00
var ps *preparedItem
data, ps, err = c.resolvePreparedSQL()
2019-07-29 07:13:33 +02:00
if err != nil {
2019-09-20 06:19:11 +02:00
return nil, err
2019-07-29 07:13:33 +02:00
}
skipped = ps.skipped
qc = ps.qc
} else {
data, skipped, err = c.resolveSQL()
2019-07-29 07:13:33 +02:00
if err != nil {
2019-09-20 06:19:11 +02:00
return nil, err
2019-07-29 07:13:33 +02:00
}
2019-05-13 01:27:26 +02:00
}
2019-04-19 07:55:03 +02:00
return c.execRemoteJoin(qc, skipped, data)
}
func (c *coreContext) execRemoteJoin(qc *qcode.QCode, skipped uint32, data []byte) ([]byte, error) {
var err error
2019-05-13 01:27:26 +02:00
if len(data) == 0 || skipped == 0 {
2019-09-20 06:19:11 +02:00
return data, nil
2019-05-13 01:27:26 +02:00
}
2019-04-19 07:55:03 +02:00
2019-09-05 06:09:56 +02:00
sel := qc.Selects
2019-05-13 01:27:26 +02:00
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)
2019-06-02 01:48:42 +02:00
var to []jsn.Field
switch {
case len(from) == 1:
2019-09-20 06:19:11 +02:00
to, err = c.resolveRemote(c.req.hdr, h, from[0], sel, sfmap)
2019-06-02 01:48:42 +02:00
case len(from) > 1:
2019-09-20 06:19:11 +02:00
to, err = c.resolveRemotes(c.req.hdr, h, from, sel, sfmap)
2019-06-02 01:48:42 +02:00
default:
2019-09-20 06:19:11 +02:00
return nil, errors.New("something wrong no remote ids found in db response")
2019-06-02 01:48:42 +02:00
}
2019-06-04 16:54:51 +02:00
if err != nil {
2019-09-20 06:19:11 +02:00
return nil, err
2019-06-04 16:54:51 +02:00
}
2019-06-02 01:48:42 +02:00
var ob bytes.Buffer
err = jsn.Replace(&ob, data, from, to)
if err != nil {
2019-09-20 06:19:11 +02:00
return nil, err
2019-06-02 01:48:42 +02:00
}
2019-09-20 06:19:11 +02:00
return ob.Bytes(), nil
2019-06-02 01:48:42 +02:00
}
func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) {
var tx pgx.Tx
var err error
mutation := isMutation(c.req.Query)
useRoleQuery := len(conf.RolesQuery) != 0 && mutation
useTx := useRoleQuery || conf.DB.SetUserID
if useTx {
if tx, err = db.Begin(c); err != nil {
return nil, nil, err
}
defer tx.Rollback(c)
}
if conf.DB.SetUserID {
if err := c.setLocalUserID(tx); err != nil {
return nil, nil, err
}
}
var role string
if useRoleQuery {
if role, err = c.executeRoleQuery(tx); err != nil {
return nil, nil, err
}
} else if v := c.Value(userRoleKey); v != nil {
role = v.(string)
} else if mutation {
role = c.req.role
}
ps, ok := _preparedList[gqlHash(c.req.Query, c.req.Vars, role)]
if !ok {
return nil, nil, errUnauthorized
}
var root []byte
var row pgx.Row
vars := argList(c, ps.args)
if useTx {
row = tx.QueryRow(c, ps.stmt.SQL, vars...)
} else {
row = db.QueryRow(c, ps.stmt.SQL, vars...)
}
if mutation {
err = row.Scan(&root)
} else {
err = row.Scan(&role, &root)
}
2019-11-19 06:47:55 +01:00
logger.Debug().Str("default_role", c.req.role).Str("role", role).Msg(c.req.Query)
if err != nil {
return nil, nil, err
}
2019-11-19 06:47:55 +01:00
c.req.role = role
if useTx {
if err := tx.Commit(c); err != nil {
return nil, nil, err
}
}
return root, ps, nil
}
func (c *coreContext) resolveSQL() ([]byte, uint32, error) {
var tx pgx.Tx
var err error
mutation := isMutation(c.req.Query)
useRoleQuery := len(conf.RolesQuery) != 0 && mutation
useTx := useRoleQuery || conf.DB.SetUserID
if useTx {
if tx, err = db.Begin(c); err != nil {
return nil, 0, err
}
defer tx.Rollback(c)
}
if conf.DB.SetUserID {
if err := c.setLocalUserID(tx); err != nil {
return nil, 0, err
}
}
if useRoleQuery {
if c.req.role, err = c.executeRoleQuery(tx); err != nil {
return nil, 0, err
}
} else if v := c.Value(userRoleKey); v != nil {
c.req.role = v.(string)
}
stmts, err := c.buildStmt()
if err != nil {
return nil, 0, err
}
var st *stmt
if mutation {
st = findStmt(c.req.role, stmts)
} else {
st = &stmts[0]
}
t := fasttemplate.New(st.sql, openVar, closeVar)
buf := &bytes.Buffer{}
_, err = t.ExecuteFunc(buf, argMap(c))
if err == errNoUserID {
logger.Warn().Msg("no user id found. query requires an authenicated request")
}
if err != nil {
return nil, 0, err
}
finalSQL := buf.String()
var stime time.Time
if conf.EnableTracing {
stime = time.Now()
}
var root []byte
var role, defaultRole string
var row pgx.Row
2019-11-19 06:47:55 +01:00
if useTx {
row = tx.QueryRow(c, finalSQL)
} else {
row = db.QueryRow(c, finalSQL)
}
if mutation {
err = row.Scan(&root)
2019-11-19 06:47:55 +01:00
} else {
err = row.Scan(&role, &root)
defaultRole = c.req.role
2019-11-19 06:47:55 +01:00
c.req.role = role
}
2019-11-19 06:47:55 +01:00
logger.Debug().Str("default_role", defaultRole).Str("role", role).Msg(c.req.Query)
2019-11-19 06:47:55 +01:00
if err != nil {
return nil, 0, err
}
if useTx {
if err := tx.Commit(c); err != nil {
return nil, 0, err
}
}
if conf.EnableTracing && len(st.qc.Selects) != 0 {
2019-11-19 06:47:55 +01:00
for _, id := range st.qc.Roots {
c.addTrace(st.qc.Selects, id, stime)
}
}
2019-11-07 08:37:24 +01:00
if conf.Production == false {
_allowList.add(&c.req)
}
return root, st.skipped, nil
}
2019-06-02 01:48:42 +02:00
func (c *coreContext) resolveRemote(
2019-09-20 06:19:11 +02:00
hdr http.Header,
2019-06-02 01:48:42 +02:00
h *xxhash.Digest,
field jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
// replacement data for the marked insertion points
// key and value will be replaced by whats below
toA := [1]jsn.Field{}
to := toA[:1]
2019-06-02 01:48:42 +02:00
// use the json key to find the related Select object
k1 := xxhash.Sum64(field.Key)
s, ok := sfmap[k1]
if !ok {
return nil, nil
}
p := sel[s.ParentID]
// 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)
r, ok := rmap[k2]
if !ok {
return nil, nil
}
id := jsn.Value(field.Value)
if len(id) == 0 {
return nil, nil
}
st := time.Now()
2019-09-20 06:19:11 +02:00
b, err := r.Fn(hdr, id)
2019-06-02 01:48:42 +02:00
if err != nil {
return nil, err
}
if conf.EnableTracing {
c.addTrace(sel, s.ID, st)
2019-06-02 01:48:42 +02:00
}
if len(r.Path) != 0 {
b = jsn.Strip(b, r.Path)
}
var ob bytes.Buffer
if len(s.Cols) != 0 {
err = jsn.Filter(&ob, b, colsToList(s.Cols))
if err != nil {
return nil, err
}
} else {
ob.WriteString("null")
}
2019-11-10 03:53:26 +01:00
to[0] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
2019-06-02 01:48:42 +02:00
return to, nil
}
func (c *coreContext) resolveRemotes(
2019-09-20 06:19:11 +02:00
hdr http.Header,
2019-06-02 01:48:42 +02:00
h *xxhash.Digest,
from []jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
2019-05-13 01:27:26 +02:00
// replacement data for the marked insertion points
// key and value will be replaced by whats below
to := make([]jsn.Field, len(from))
2019-05-13 01:27:26 +02:00
var wg sync.WaitGroup
wg.Add(len(from))
var cerr error
for i, id := range from {
2019-06-02 01:48:42 +02:00
2019-05-13 01:27:26 +02:00
// use the json key to find the related Select object
k1 := xxhash.Sum64(id.Key)
s, ok := sfmap[k1]
if !ok {
2019-06-02 01:48:42 +02:00
return nil, nil
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 {
2019-06-02 01:48:42 +02:00
return nil, nil
2019-05-13 01:27:26 +02:00
}
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 {
2019-06-02 01:48:42 +02:00
return nil, nil
2019-04-20 06:35:57 +02:00
}
go func(n int, id []byte, s *qcode.Select) {
defer wg.Done()
2019-04-20 06:35:57 +02:00
st := time.Now()
2019-04-20 06:35:57 +02:00
2019-09-20 06:19:11 +02:00
b, err := r.Fn(hdr, id)
if err != nil {
2019-06-04 16:54:51 +02:00
cerr = fmt.Errorf("%s: %s", s.Table, err)
return
}
2019-05-13 01:27:26 +02:00
if conf.EnableTracing {
c.addTrace(sel, s.ID, st)
}
2019-05-13 01:27:26 +02:00
if len(r.Path) != 0 {
b = jsn.Strip(b, r.Path)
2019-05-13 06:05:08 +02:00
}
var ob bytes.Buffer
if len(s.Cols) != 0 {
err = jsn.Filter(&ob, b, colsToList(s.Cols))
if err != nil {
2019-06-04 16:54:51 +02:00
cerr = fmt.Errorf("%s: %s", s.Table, err)
return
}
2019-05-13 01:27:26 +02:00
} else {
ob.WriteString("null")
}
2019-11-10 03:53:26 +01:00
to[n] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
}(i, id, s)
2019-05-13 01:27:26 +02:00
}
wg.Wait()
return to, cerr
2019-05-13 01:27:26 +02:00
}
func (c *coreContext) executeRoleQuery(tx pgx.Tx) (string, error) {
var role string
row := tx.QueryRow(c, "_sg_get_role", c.req.role, 1)
2019-04-19 07:55:03 +02:00
if err := row.Scan(&role); err != nil {
return "", err
2019-07-29 07:13:33 +02:00
}
return role, nil
2019-05-13 01:27:26 +02:00
}
func (c *coreContext) setLocalUserID(tx pgx.Tx) error {
var err error
if v := c.Value(userIDKey); v != nil {
_, err = tx.Exec(c, fmt.Sprintf(`SET LOCAL "user.id" = %s;`, v))
}
return err
}
2019-05-13 01:27:26 +02:00
func (c *coreContext) render(w io.Writer, data []byte) error {
c.res.Data = json.RawMessage(data)
return json.NewEncoder(w).Encode(c.res)
}
2019-06-08 02:53:08 +02:00
func (c *coreContext) addTrace(sel []qcode.Select, id int32, st time.Time) {
2019-05-13 06:05:08 +02:00
et := time.Now()
du := et.Sub(st)
if c.res.Extensions == nil {
c.res.Extensions = &extensions{&trace{
Version: 1,
StartTime: st,
Execution: execution{},
}}
}
c.res.Extensions.Tracing.EndTime = et
c.res.Extensions.Tracing.Duration = du
2019-06-08 02:53:08 +02:00
n := 1
2019-11-19 06:47:55 +01:00
for i := id; i != -1; i = sel[i].ParentID {
n++
}
path := make([]string, n)
2019-06-08 02:53:08 +02:00
n--
2019-06-08 02:53:08 +02:00
for i := id; ; i = sel[i].ParentID {
path[n] = sel[i].Table
2019-11-19 06:47:55 +01:00
if sel[i].ParentID == -1 {
2019-06-08 02:53:08 +02:00
break
}
n--
}
2019-05-13 06:05:08 +02:00
tr := resolver{
Path: path,
2019-05-13 06:05:08 +02:00
ParentType: "Query",
FieldName: sel[id].Table,
2019-05-13 06:05:08 +02:00
ReturnType: "object",
StartOffset: 1,
Duration: du,
}
c.res.Extensions.Tracing.Execution.Resolvers =
append(c.res.Extensions.Tracing.Execution.Resolvers, tr)
}
2019-05-13 01:27:26 +02:00
func parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) (
[][]byte,
map[uint64]*qcode.Select) {
c := 0
for i := range sel {
s := &sel[i]
2019-06-08 02:53:08 +02:00
if isSkipped(skipped, uint32(s.ID)) {
2019-05-13 01:27:26 +02:00
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]
2019-06-08 02:53:08 +02:00
if isSkipped(skipped, uint32(s.ID)) == false {
2019-05-13 01:27:26 +02:00
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
}
2019-06-08 02:53:08 +02:00
func isSkipped(n uint32, pos uint32) bool {
2019-05-13 01:27:26 +02:00
return ((n & (1 << pos)) != 0)
}
func authCheck(ctx *coreContext) bool {
return (ctx.Value(userIDKey) != nil)
}
2019-05-13 06:05:08 +02:00
func colsToList(cols []qcode.Column) []string {
var f []string
2019-05-13 01:27:26 +02:00
2019-05-13 06:05:08 +02:00
for i := range cols {
f = append(f, cols[i].Name)
2019-04-19 07:55:03 +02:00
}
2019-05-13 06:05:08 +02:00
return f
2019-04-19 07:55:03 +02:00
}