diff --git a/README.md b/README.md index 25c1865..aece066 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,14 @@ I wanted a GraphQL server that just worked the second you deployed it without ha And so after a lot of coffee and some avocado toasts Super Graph was born. An instant GraphQL API service that's high performance and easy to deploy. I hope you find it as useful as I do and there's a lot more coming so hit that :star: to stay in the loop. ## Features -- Support for Rails database conventions +- Works with Rails database schemas +- Automatically learns schemas and relationships - Belongs-To, One-To-Many and Many-To-Many table relationships -- Devise, Warden encrypted and signed session cookies -- Redis, Memcache and Cookie session stores -- JWT tokens supported from providers like Auth0 -- Generates highly optimized and fast Postgres SQL queries -- Customize through a simple config file +- Full text search and Aggregations +- Rails Auth supported (Redis, Memcache, Cookie) +- JWT tokens supported (Auth0, etc) +- Highly optimized and fast Postgres SQL queries +- Configure with a simple config file - High performance GO codebase - Tiny docker image and low memory requirements diff --git a/psql/psql.go b/psql/psql.go index 891199b..14519b7 100644 --- a/psql/psql.go +++ b/psql/psql.go @@ -274,7 +274,7 @@ func (v *selectBlock) renderBaseSelect(w io.Writer, schema *DBSchema, childCols isFil := v.sel.Where != nil isAgg := false - searchVal := findArgVal(v.sel, "search") + _, isSearch := v.sel.Args["search"] io.WriteString(w, " FROM (SELECT ") @@ -284,15 +284,19 @@ func (v *selectBlock) renderBaseSelect(w io.Writer, schema *DBSchema, childCols if !isRealCol { switch { - case searchVal != nil && cn == "search_rank": + case isSearch && cn == "search_rank": cn = v.ti.TSVCol - fmt.Fprintf(w, `ts_rank("%s"."%s", to_tsquery('%s')) AS %s`, - v.sel.Table, cn, searchVal.Val, col.Name) + arg := v.sel.Args["search"] - case searchVal != nil && strings.HasPrefix(cn, "search_headline_"): + fmt.Fprintf(w, `ts_rank("%s"."%s", to_tsquery('%s')) AS %s`, + v.sel.Table, cn, arg.Val, col.Name) + + case isSearch && strings.HasPrefix(cn, "search_headline_"): cn = cn[16:] + arg := v.sel.Args["search"] + fmt.Fprintf(w, `ts_headline("%s"."%s", to_tsquery('%s')) AS %s`, - v.sel.Table, cn, searchVal.Val, col.Name) + v.sel.Table, cn, arg.Val, col.Name) default: pl := funcPrefixLen(cn) @@ -647,12 +651,3 @@ func funcPrefixLen(fn string) int { } return 0 } - -func findArgVal(sel *qcode.Select, name string) *qcode.Node { - for i := range sel.Args { - if sel.Args[i].Name == name { - return sel.Args[i].Val - } - } - return nil -} diff --git a/psql/utils.go b/psql/utils.go index b52bcc7..195d294 100644 --- a/psql/utils.go +++ b/psql/utils.go @@ -4,7 +4,7 @@ import "regexp" func NewVariables(varlist map[string]string) map[string]string { re := regexp.MustCompile(`(?mi)\$([a-zA-Z0-9_.]+)`) - vars := make(map[string]string) + vars := make(map[string]string, len(varlist)) for k, v := range varlist { vars[k] = re.ReplaceAllString(v, `{{$1}}`) diff --git a/qcode/parse.go b/qcode/parse.go index b9ab20e..0d1970d 100644 --- a/qcode/parse.go +++ b/qcode/parse.go @@ -59,13 +59,15 @@ func (t parserType) String() string { } type Operation struct { - Type parserType - Name string - Args []*Arg - Fields []*Field + Type parserType + Name string + Args []*Arg + Fields []*Field + FieldLen int16 } type Field struct { + ID int16 Name string Alias string Args []*Arg @@ -200,10 +202,12 @@ func (p *Parser) parseOpByType(ty parserType) (*Operation, error) { if p.peek(itemObjOpen) { p.ignore() - op.Fields, err = p.parseFields() + n := int16(0) + op.Fields, n, err = p.parseFields() if err != nil { return nil, err } + op.FieldLen = n } if p.peek(itemObjClose) { @@ -233,9 +237,10 @@ func (p *Parser) parseOp() (*Operation, error) { return nil, errors.New("unknown operation type") } -func (p *Parser) parseFields() ([]*Field, error) { +func (p *Parser) parseFields() ([]*Field, int16, error) { var roots []*Field st := util.NewStack() + i := int16(0) for { if p.peek(itemObjClose) { @@ -248,14 +253,20 @@ func (p *Parser) parseFields() ([]*Field, error) { continue } + if i > 500 { + return nil, 0, errors.New("too many fields") + } + if p.peek(itemName) == false { - return nil, errors.New("expecting an alias or field name") + return nil, 0, errors.New("expecting an alias or field name") } field, err := p.parseField() if err != nil { - return nil, err + return nil, 0, err } + field.ID = i + i++ if st.Len() == 0 { roots = append(roots, field) @@ -264,7 +275,7 @@ func (p *Parser) parseFields() ([]*Field, error) { intf := st.Peek() parent, ok := intf.(*Field) if !ok || parent == nil { - return nil, fmt.Errorf("unexpected value encountered %v", intf) + return nil, 0, fmt.Errorf("unexpected value encountered %v", intf) } field.Parent = parent parent.Children = append(parent.Children, field) @@ -276,7 +287,7 @@ func (p *Parser) parseFields() ([]*Field, error) { } } - return roots, nil + return roots, i, nil } func (p *Parser) parseField() (*Field, error) { diff --git a/qcode/qcode.go b/qcode/qcode.go index da712ae..1a083a9 100644 --- a/qcode/qcode.go +++ b/qcode/qcode.go @@ -2,7 +2,6 @@ package qcode import ( "fmt" - "regexp" "strings" "github.com/dosco/super-graph/util" @@ -25,7 +24,7 @@ type Column struct { type Select struct { ID int16 - Args []*Arg + Args map[string]*Node AsList bool Table string Singular string @@ -181,7 +180,7 @@ const ( ) type FilterMap map[string]*Exp -type Blacklist *regexp.Regexp +type Blacklist map[string]struct{} func CompileFilter(filter string) (*Exp, error) { node, err := ParseArgValue(filter) @@ -194,7 +193,7 @@ func CompileFilter(filter string) (*Exp, error) { type Compiler struct { fm FilterMap - bl *regexp.Regexp + bl Blacklist } func NewCompiler(fm FilterMap, bl Blacklist) *Compiler { @@ -231,7 +230,7 @@ func (com *Compiler) compileQuery(op *Operation) (*Query, error) { st := util.NewStack() id := int16(0) - fmap := make(map[*Field]*Select) + fs := make([]*Select, op.FieldLen) for i := range op.Fields { st.Push(op.Fields[i]) @@ -249,11 +248,10 @@ func (com *Compiler) compileQuery(op *Operation) (*Query, error) { return nil, fmt.Errorf("unexpected value poped out %v", intf) } - if com.bl != nil && com.bl.MatchString(field.Name) { + fn := strings.ToLower(field.Name) + if _, ok := com.bl[fn]; ok { continue } - - fn := strings.ToLower(field.Name) tn := flect.Pluralize(fn) s := &Select{ @@ -282,7 +280,7 @@ func (com *Compiler) compileQuery(op *Operation) (*Query, error) { } id++ - fmap[field] = s + fs[field.ID] = s err := com.compileArgs(s, field.Args) if err != nil { @@ -293,7 +291,7 @@ func (com *Compiler) compileQuery(op *Operation) (*Query, error) { f := field.Children[i] fn := strings.ToLower(f.Name) - if com.bl != nil && com.bl.MatchString(fn) { + if _, ok := com.bl[fn]; ok { continue } @@ -313,10 +311,9 @@ func (com *Compiler) compileQuery(op *Operation) (*Query, error) { if field.Parent == nil { selRoot = s - } else if sp, ok := fmap[field.Parent]; ok { - sp.Joins = append(sp.Joins, s) } else { - return nil, fmt.Errorf("no select found for parent %#v", field.Parent) + sp := fs[field.Parent.ID] + sp.Joins = append(sp.Joins, s) } } @@ -333,14 +330,15 @@ func (com *Compiler) compileQuery(op *Operation) (*Query, error) { func (com *Compiler) compileArgs(sel *Select, args []*Arg) error { var err error - ad := make(map[string]struct{}) + + sel.Args = make(map[string]*Node, len(args)) for i := range args { if args[i] == nil { return fmt.Errorf("[Args] unexpected nil argument found") } an := strings.ToLower(args[i].Name) - if _, ok := ad[an]; ok { + if _, ok := sel.Args[an]; ok { continue } @@ -367,7 +365,7 @@ func (com *Compiler) compileArgs(sel *Select, args []*Arg) error { return err } - ad[an] = struct{}{} + sel.Args[an] = args[i].Val } return nil @@ -380,7 +378,7 @@ type expT struct { func (com *Compiler) compileArgObj(arg *Arg) (*Exp, error) { if arg.Val.Type != nodeObj { - return nil, fmt.Errorf("[Where] expecting an object") + return nil, fmt.Errorf("expecting an object") } return com.compileArgNode(arg.Val) @@ -400,12 +398,13 @@ func (com *Compiler) compileArgNode(val *Node) (*Exp, error) { intf := st.Pop() eT, ok := intf.(*expT) if !ok || eT == nil { - return nil, fmt.Errorf("[Where] unexpected value poped out %v", intf) + return nil, fmt.Errorf("unexpected value poped out %v", intf) } - if len(eT.node.Name) != 0 && - com.bl != nil && com.bl.MatchString(eT.node.Name) { - continue + if len(eT.node.Name) != 0 { + if _, ok := com.bl[strings.ToLower(eT.node.Name)]; ok { + continue + } } ex, err := newExp(st, eT) @@ -457,8 +456,6 @@ func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) error { Val: arg.Val.Val, } - sel.Args = append(sel.Args, arg) - if sel.Where != nil { sel.Where = &Exp{Op: OpAnd, Children: []*Exp{ex, sel.Where}} } else { @@ -507,7 +504,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error { return fmt.Errorf("OrderBy: unexpected value poped out %v", intf) } - if com.bl != nil && com.bl.MatchString(node.Name) { + if _, ok := com.bl[strings.ToLower(node.Name)]; ok { continue } @@ -547,7 +544,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error { func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) error { node := arg.Val - if com.bl != nil && com.bl.MatchString(node.Name) { + if _, ok := com.bl[strings.ToLower(node.Name)]; ok { return nil } diff --git a/qcode/utils.go b/qcode/utils.go index 065b383..3dad198 100644 --- a/qcode/utils.go +++ b/qcode/utils.go @@ -1,17 +1,14 @@ package qcode import ( - "fmt" - "regexp" "strings" ) -func NewBlacklist(list []string) *regexp.Regexp { - var bl *regexp.Regexp +func NewBlacklist(list []string) Blacklist { + bl := make(map[string]struct{}, len(list)) - if len(list) != 0 { - re := fmt.Sprintf("(?i)%s", strings.Join(list, "|")) - bl = regexp.MustCompile(re) + for i := range list { + bl[strings.ToLower(list[i])] = struct{}{} } return bl }