From b93d2c3c3ba17845e33fe7da489a5942ac2a874e Mon Sep 17 00:00:00 2001 From: dosco <832235+dosco@users.noreply.github.com> Date: Thu, 1 Dec 2022 17:01:46 -0800 Subject: [PATCH] feat: add support for using custom db functions as fields and tables #394 1. db functions can now be used as fields or tables with suppot for named parameters 2. added top level __typename to work with fluter_graphql, etc #404 --- core/core_test.go | 374 -------------------------------- core/cue_test.go | 391 ++++++++++++++++++++++++++++++++++ core/internal/psql/columns.go | 95 +-------- core/internal/psql/fn.go | 98 +++++++++ core/internal/psql/query.go | 30 +-- core/internal/psql/recur.go | 2 +- core/internal/qcode/fields.go | 102 ++++++--- core/internal/qcode/fn.go | 9 +- core/internal/qcode/qcode.go | 46 ++-- core/query_pg_test.go | 73 ++++--- core/query_test.go | 25 +++ 11 files changed, 665 insertions(+), 580 deletions(-) create mode 100644 core/cue_test.go create mode 100644 core/internal/psql/fn.go diff --git a/core/core_test.go b/core/core_test.go index 5c2c67b4..e28165b4 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -364,377 +364,3 @@ func BenchmarkCompile(b *testing.B) { resultJSON = res.Data } } -func TestCueValidationQuerySingleIntVarValue(t *testing.T) { - gql := `query @validation(cue:"id:2") { - users(where: {id:$id}) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":2}`), nil) - if err != nil { - t.Error(err) - return - } -} -func TestCueInvalidationQuerySingleIntVarValue(t *testing.T) { - gql := `query @validation(cue:"id:2") { - users(where: {id:$id}) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":3}`), nil) - if err == nil { - t.Error("expected validation error") - } -} -func TestCueValidationQuerySingleIntVarType(t *testing.T) { - gql := `query @validation(cue:"id:int") { - users(where: {id:$id}) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":2}`), nil) - if err != nil { - t.Error(err) - return - } -} -func TestCueValidationQuerySingleIntVarOR(t *testing.T) { - gql := `query @validation(cue:"id: 3 | 2") { - users(where: {id:$id}) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":2}`), nil) - if err != nil { - t.Error(err) - return - } -} -func TestCueInvalidationQuerySingleIntVarOR(t *testing.T) { - gql := `query @validation(cue:"id: 3 | 2") { - users(where: {id:$id}) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":4}`), nil) - if err == nil { - t.Error(err) - } -} -func TestCueValidationQuerySingleStringVarOR(t *testing.T) { - // TODO: couldn't find a way to pass string inside cue through plain graphql query ( " ) - // (only way is using varibales and escape \") - gql := `query @validation(cue:$validation) { - users(where: {email:$mail}) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"mail":"mail@example.com","validation":"mail: \"mail@example.com\" | \"mail@example.org\" "}`), nil) - if err != nil { - t.Error(err) - return - } -} -func TestCueInvalidationQuerySingleStringVarOR(t *testing.T) { - gql := `query @validation(cue:$validation) { - users(where: {email:$mail}) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"mail":"mail@example.net","validation":"mail: \"mail@example.com\" | \"mail@example.org\" "}`), nil) - if err == nil { - t.Error(err) - } -} -func TestCueInvalidationQuerySingleIntVarType(t *testing.T) { - gql := `query @validation(cue:"email:int") { - users(where: {email:$email}) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"email":"mail@example.com"}`), nil) - if err == nil { - t.Error("expected validation error") - } -} -func TestCueValidationMutationMapVarStringsLen(t *testing.T) { - if dbType == "mysql" { - t.SkipNow() - return - } - gql := `mutation @validation(cue:$validation) { - users(insert:$inp) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ - "inp":{ - "id":105, "email":"mail1@example.com", "full_name":"Full Name", "created_at":"now", "updated_at":"now" - }, - "validation":"import (\"strings\"), inp: {id?: int, full_name: string & strings.MinRunes(3) & strings.MaxRunes(22), created_at:\"now\", updated_at:\"now\", email: string}" - }`), nil) - if err != nil { - t.Error(err) - return - } -} -func TestCueInvalidationMutationMapVarStringsLen(t *testing.T) { - if dbType == "mysql" { - t.SkipNow() - return - } - gql := `mutation @validation(cue:$validation) { - users(insert:$inp) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ - "inp":{ - "id":106, "email":"mail2@example.com", "full_name":"Fu", "created_at":"now", "updated_at":"now" - }, - "validation":"import (\"strings\"), inp: {id?: int, full_name: string & strings.MinRunes(3) & strings.MaxRunes(22), created_at:\"now\", updated_at:\"now\", email: string}" - }`), nil) - if err == nil { - t.Error(err) - } -} - -func TestCueValidationMutationMapVarIntMaxMin(t *testing.T) { - if dbType == "mysql" { - t.SkipNow() - return - } - gql := `mutation @validation(cue:$validation) { - users(insert:$inp) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ - "inp":{ - "id":101, "email":"mail3@example.com", "full_name":"Full Name", "created_at":"now", "updated_at":"now" - }, - "validation":" inp: {id?: int & >100 & <102, full_name: string , created_at:\"now\", updated_at:\"now\", email: string}" - }`), nil) - if err != nil { - t.Error(err) - return - } -} -func TestCueInvalidationMutationMapVarIntMaxMin(t *testing.T) { - if dbType == "mysql" { - t.SkipNow() - return - } - gql := `mutation @validation(cue:$validation) { - users(insert:$inp) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ - "inp":{ - "id":107, "email":"mail4@example.com", "full_name":"Fu", "created_at":"now", "updated_at":"now" - }, - "validation":"inp: {id?: int & >100 & <102, full_name: string , created_at:\"now\", updated_at:\"now\", email: string}" - }`), nil) - if err == nil { - t.Error(err) - } -} -func TestCueValidationMutationMapVarOptionalKey(t *testing.T) { - if dbType == "mysql" { - t.SkipNow() - return - } - gql := `mutation @validation(cue:$validation) { - users(insert:$inp) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ - "inp":{ - "id":111, "email":"mail7@example.com", "full_name":"Fu", "created_at":"now", "updated_at":"now" - }, - "validation":"inp: {id?: int, phone?: string, full_name: string , created_at:\"now\", updated_at:\"now\", email: string}" - }`), nil) - if err != nil { - t.Error(err) - return - } -} -func TestCueValidationMutationMapVarRegex(t *testing.T) { - if dbType == "mysql" { - t.SkipNow() - return - } - gql := `mutation @validation(cue:$validation) { - users(insert:$inp) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ - "inp":{ - "id":108, "email":"mail5@example.com", "full_name":"Full Name", "created_at":"now", "updated_at":"now" - }, - "validation":"inp: {id?: int & >100 & <110, full_name: string , created_at:\"now\", updated_at:\"now\", email: =~\"^[a-zA-Z0-9.!#$+%&'*/=?^_{|}\\\\-`+"`"+`~]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$\"}" - }`), nil) // regex from : https://cuelang.org/play/?id=iFcZKx72Bwm#cue@export@cue - if err != nil { - t.Error(err) - return - } -} -func TestCueInvalidationMutationMapVarRegex(t *testing.T) { - if dbType == "mysql" { - t.SkipNow() - return - } - gql := `mutation @validation(cue:$validation) { - users(insert:$inp) { - id - full_name - email - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ - "inp":{ - "id":109, "email":"mail6@ex`+"`"+`ample.com", "full_name":"Full Name", "created_at":"now", "updated_at":"now" - }, - "validation":"inp: {id?: int & >110 & <102, full_name: string , created_at:\"now\", updated_at:\"now\", email: =~\"^[a-zA-Z0-9.!#$+%&'*/=?^_{|}\\\\-`+"`"+`~]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$\"}" - }`), nil) - if err == nil { - t.Error(err) - } -} diff --git a/core/cue_test.go b/core/cue_test.go new file mode 100644 index 00000000..161fdece --- /dev/null +++ b/core/cue_test.go @@ -0,0 +1,391 @@ +package core_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/dosco/graphjin/core" +) + +func TestCueValidationQuerySingleIntVarValue(t *testing.T) { + gql := `query @validation(cue:"id:2") { + users(where: {id:$id}) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":2}`), nil) + if err != nil { + t.Error(err) + return + } +} + +func TestCueInvalidationQuerySingleIntVarValue(t *testing.T) { + gql := `query @validation(cue:"id:2") { + users(where: {id:$id}) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":3}`), nil) + if err == nil { + t.Error("expected validation error") + } +} + +func TestCueValidationQuerySingleIntVarType(t *testing.T) { + gql := `query @validation(cue:"id:int") { + users(where: {id:$id}) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":2}`), nil) + if err != nil { + t.Error(err) + return + } +} + +func TestCueValidationQuerySingleIntVarOR(t *testing.T) { + gql := `query @validation(cue:"id: 3 | 2") { + users(where: {id:$id}) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":2}`), nil) + if err != nil { + t.Error(err) + return + } +} + +func TestCueInvalidationQuerySingleIntVarOR(t *testing.T) { + gql := `query @validation(cue:"id: 3 | 2") { + users(where: {id:$id}) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":4}`), nil) + if err == nil { + t.Error(err) + } +} + +func TestCueValidationQuerySingleStringVarOR(t *testing.T) { + // TODO: couldn't find a way to pass string inside cue through plain graphql query ( " ) + // (only way is using varibales and escape \") + gql := `query @validation(cue:$validation) { + users(where: {email:$mail}) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"mail":"mail@example.com","validation":"mail: \"mail@example.com\" | \"mail@example.org\" "}`), nil) + if err != nil { + t.Error(err) + return + } +} + +func TestCueInvalidationQuerySingleStringVarOR(t *testing.T) { + gql := `query @validation(cue:$validation) { + users(where: {email:$mail}) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"mail":"mail@example.net","validation":"mail: \"mail@example.com\" | \"mail@example.org\" "}`), nil) + if err == nil { + t.Error(err) + } +} +func TestCueInvalidationQuerySingleIntVarType(t *testing.T) { + gql := `query @validation(cue:"email:int") { + users(where: {email:$email}) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{"email":"mail@example.com"}`), nil) + if err == nil { + t.Error("expected validation error") + } +} + +func TestCueValidationMutationMapVarStringsLen(t *testing.T) { + if dbType == "mysql" { + t.SkipNow() + return + } + gql := `mutation @validation(cue:$validation) { + users(insert:$inp) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ + "inp":{ + "id":105, "email":"mail1@example.com", "full_name":"Full Name", "created_at":"now", "updated_at":"now" + }, + "validation":"import (\"strings\"), inp: {id?: int, full_name: string & strings.MinRunes(3) & strings.MaxRunes(22), created_at:\"now\", updated_at:\"now\", email: string}" + }`), nil) + if err != nil { + t.Error(err) + return + } +} +func TestCueInvalidationMutationMapVarStringsLen(t *testing.T) { + if dbType == "mysql" { + t.SkipNow() + return + } + gql := `mutation @validation(cue:$validation) { + users(insert:$inp) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ + "inp":{ + "id":106, "email":"mail2@example.com", "full_name":"Fu", "created_at":"now", "updated_at":"now" + }, + "validation":"import (\"strings\"), inp: {id?: int, full_name: string & strings.MinRunes(3) & strings.MaxRunes(22), created_at:\"now\", updated_at:\"now\", email: string}" + }`), nil) + if err == nil { + t.Error(err) + } +} + +func TestCueValidationMutationMapVarIntMaxMin(t *testing.T) { + if dbType == "mysql" { + t.SkipNow() + return + } + gql := `mutation @validation(cue:$validation) { + users(insert:$inp) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ + "inp":{ + "id":101, "email":"mail3@example.com", "full_name":"Full Name", "created_at":"now", "updated_at":"now" + }, + "validation":" inp: {id?: int & >100 & <102, full_name: string , created_at:\"now\", updated_at:\"now\", email: string}" + }`), nil) + if err != nil { + t.Error(err) + return + } +} +func TestCueInvalidationMutationMapVarIntMaxMin(t *testing.T) { + if dbType == "mysql" { + t.SkipNow() + return + } + gql := `mutation @validation(cue:$validation) { + users(insert:$inp) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ + "inp":{ + "id":107, "email":"mail4@example.com", "full_name":"Fu", "created_at":"now", "updated_at":"now" + }, + "validation":"inp: {id?: int & >100 & <102, full_name: string , created_at:\"now\", updated_at:\"now\", email: string}" + }`), nil) + if err == nil { + t.Error(err) + } +} +func TestCueValidationMutationMapVarOptionalKey(t *testing.T) { + if dbType == "mysql" { + t.SkipNow() + return + } + gql := `mutation @validation(cue:$validation) { + users(insert:$inp) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ + "inp":{ + "id":111, "email":"mail7@example.com", "full_name":"Fu", "created_at":"now", "updated_at":"now" + }, + "validation":"inp: {id?: int, phone?: string, full_name: string , created_at:\"now\", updated_at:\"now\", email: string}" + }`), nil) + if err != nil { + t.Error(err) + return + } +} +func TestCueValidationMutationMapVarRegex(t *testing.T) { + if dbType == "mysql" { + t.SkipNow() + return + } + gql := `mutation @validation(cue:$validation) { + users(insert:$inp) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ + "inp":{ + "id":108, "email":"mail5@example.com", "full_name":"Full Name", "created_at":"now", "updated_at":"now" + }, + "validation":"inp: {id?: int & >100 & <110, full_name: string , created_at:\"now\", updated_at:\"now\", email: =~\"^[a-zA-Z0-9.!#$+%&'*/=?^_{|}\\\\-`+"`"+`~]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$\"}" + }`), nil) // regex from : https://cuelang.org/play/?id=iFcZKx72Bwm#cue@export@cue + if err != nil { + t.Error(err) + return + } +} +func TestCueInvalidationMutationMapVarRegex(t *testing.T) { + if dbType == "mysql" { + t.SkipNow() + return + } + gql := `mutation @validation(cue:$validation) { + users(insert:$inp) { + id + full_name + email + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + _, err = gj.GraphQL(context.Background(), gql, json.RawMessage(`{ + "inp":{ + "id":109, "email":"mail6@ex`+"`"+`ample.com", "full_name":"Full Name", "created_at":"now", "updated_at":"now" + }, + "validation":"inp: {id?: int & >110 & <102, full_name: string , created_at:\"now\", updated_at:\"now\", email: =~\"^[a-zA-Z0-9.!#$+%&'*/=?^_{|}\\\\-`+"`"+`~]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$\"}" + }`), nil) + if err == nil { + t.Error(err) + } +} diff --git a/core/internal/psql/columns.go b/core/internal/psql/columns.go index bf3707b6..6c044e2a 100644 --- a/core/internal/psql/columns.go +++ b/core/internal/psql/columns.go @@ -46,17 +46,6 @@ func (c *compilerContext) renderFuncColumn(sel *qcode.Select, f qcode.Field) { c.colWithTableID(sel.Table, sel.ID, f.FieldName) } -func (c *compilerContext) renderFunction(sel *qcode.Select, f qcode.Field) { - switch f.Func.Name { - case "search_rank": - c.renderFunctionSearchRank(sel, f) - case "search_headline": - c.renderFunctionSearchHeadline(sel, f) - default: - c.renderOtherFunction(sel, f) - } -} - func (c *compilerContext) renderJoinColumns(sel *qcode.Select, n int) { i := n for _, cid := range sel.Children { @@ -129,85 +118,6 @@ func (c *compilerContext) renderUnionColumn(sel, csel *qcode.Select) { c.alias(csel.FieldName) } -func (c *compilerContext) renderFunctionSearchRank(sel *qcode.Select, f qcode.Field) { - if c.ct == "mysql" { - c.w.WriteString(`0`) - return - } - - c.w.WriteString(`ts_rank(`) - for i, col := range sel.Ti.FullText { - if i != 0 { - c.w.WriteString(` || `) - } - c.colWithTable(sel.Table, col.Name) - } - if c.cv >= 110000 { - c.w.WriteString(`, websearch_to_tsquery(`) - } else { - c.w.WriteString(`, to_tsquery(`) - } - arg, _ := sel.GetArg("search") - c.renderParam(Param{Name: arg.Val, Type: "text"}) - c.w.WriteString(`))`) -} - -func (c *compilerContext) renderFunctionSearchHeadline(sel *qcode.Select, f qcode.Field) { - if c.ct == "mysql" { - c.w.WriteString(`''`) - return - } - - c.w.WriteString(`ts_headline(`) - c.colWithTable(sel.Table, f.Col.Name) - if c.cv >= 110000 { - c.w.WriteString(`, websearch_to_tsquery(`) - } else { - c.w.WriteString(`, to_tsquery(`) - } - arg, _ := sel.GetArg("search") - c.renderParam(Param{Name: arg.Val, Type: "text"}) - c.w.WriteString(`))`) -} - -func (c *compilerContext) renderOtherFunction(sel *qcode.Select, f qcode.Field) { - c.w.WriteString(f.Func.Name) - c.w.WriteString(`(`) - - i := 0 - for _, a := range f.Args { - if a.Name == "" { - if i != 0 { - c.w.WriteString(`, `) - } - c.renderFuncArgVal(a) - } - i++ - } - for _, a := range f.Args { - if a.Name != "" { - if i != 0 { - c.w.WriteString(`, `) - } - c.w.WriteString(a.Name + ` => `) - c.renderFuncArgVal(a) - } - i++ - } - _, _ = c.w.WriteString(`)`) -} - -func (c *compilerContext) renderFuncArgVal(a qcode.Arg) { - switch a.Type { - case qcode.ArgTypeCol: - c.colWithTable(a.Col.Table, a.Col.Name) - case qcode.ArgTypeVar: - fallthrough - default: - c.squoted(a.Val) - } -} - func (c *compilerContext) renderBaseColumns(sel *qcode.Select) { i := 0 for _, col := range sel.BCols { @@ -230,7 +140,7 @@ func (c *compilerContext) renderBaseColumns(sel *qcode.Select) { c.renderExp(sel.Ti, f.FieldFilter.Exp, false) c.w.WriteString(` THEN `) } - c.renderFunction(sel, f) + c.renderFieldFunction(sel, f) if f.FieldFilter.Exp != nil { c.w.WriteString(` ELSE null END)`) @@ -241,9 +151,8 @@ func (c *compilerContext) renderBaseColumns(sel *qcode.Select) { } func (c *compilerContext) renderTypename(sel *qcode.Select) { - c.w.WriteString(`(`) c.squoted(sel.Table) - c.w.WriteString(`) AS "__typename"`) + c.w.WriteString(` AS "__typename"`) } func (c *compilerContext) renderJSONFields(sel *qcode.Select) { diff --git a/core/internal/psql/fn.go b/core/internal/psql/fn.go new file mode 100644 index 00000000..601fdf79 --- /dev/null +++ b/core/internal/psql/fn.go @@ -0,0 +1,98 @@ +package psql + +import "github.com/dosco/graphjin/core/internal/qcode" + +func (c *compilerContext) renderFunctionSearchRank(sel *qcode.Select, f qcode.Field) { + if c.ct == "mysql" { + c.w.WriteString(`0`) + return + } + + c.w.WriteString(`ts_rank(`) + for i, col := range sel.Ti.FullText { + if i != 0 { + c.w.WriteString(` || `) + } + c.colWithTable(sel.Table, col.Name) + } + if c.cv >= 110000 { + c.w.WriteString(`, websearch_to_tsquery(`) + } else { + c.w.WriteString(`, to_tsquery(`) + } + arg, _ := sel.GetInternalArg("search") + c.renderParam(Param{Name: arg.Val, Type: "text"}) + c.w.WriteString(`))`) +} + +func (c *compilerContext) renderFunctionSearchHeadline(sel *qcode.Select, f qcode.Field) { + if c.ct == "mysql" { + c.w.WriteString(`''`) + return + } + + c.w.WriteString(`ts_headline(`) + c.colWithTable(sel.Table, f.Col.Name) + if c.cv >= 110000 { + c.w.WriteString(`, websearch_to_tsquery(`) + } else { + c.w.WriteString(`, to_tsquery(`) + } + arg, _ := sel.GetInternalArg("search") + c.renderParam(Param{Name: arg.Val, Type: "text"}) + c.w.WriteString(`))`) +} + +func (c *compilerContext) renderTableFunction(sel *qcode.Select) { + c.renderFunction(sel.Table, sel.Args) + c.alias(sel.Table) +} + +func (c *compilerContext) renderFieldFunction(sel *qcode.Select, f qcode.Field) { + switch f.Func.Name { + case "search_rank": + c.renderFunctionSearchRank(sel, f) + case "search_headline": + c.renderFunctionSearchHeadline(sel, f) + default: + c.renderFunction(f.Func.Name, f.Args) + } +} + +func (c *compilerContext) renderFunction(name string, args []qcode.Arg) { + c.w.WriteString(name) + c.w.WriteString(`(`) + + i := 0 + for _, a := range args { + if a.Name == "" { + if i != 0 { + c.w.WriteString(`, `) + } + c.renderFuncArgVal(a) + i++ + } + } + for _, a := range args { + if a.Name != "" { + if i != 0 { + c.w.WriteString(`, `) + } + c.w.WriteString(a.Name + ` => `) + c.renderFuncArgVal(a) + i++ + } + } + _, _ = c.w.WriteString(`)`) +} + +func (c *compilerContext) renderFuncArgVal(a qcode.Arg) { + switch a.Type { + case qcode.ArgTypeCol: + c.colWithTable(a.Col.Table, a.Col.Name) + case qcode.ArgTypeVar: + c.renderParam(Param{Name: a.Val, Type: a.ValType}) + default: + c.squoted(a.Val) + } +} diff --git a/core/internal/psql/query.go b/core/internal/psql/query.go index 98f28903..6a6cb5ac 100644 --- a/core/internal/psql/query.go +++ b/core/internal/psql/query.go @@ -126,6 +126,12 @@ func (co *Compiler) CompileQuery( default: c.w.WriteString(`SELECT jsonb_build_object(`) } + if qc.Typename { + c.w.WriteString(`'__typename', `) + c.squoted(qc.Name) + i++ + } + for _, id := range qc.Roots { if i != 0 { c.w.WriteString(`, `) @@ -459,7 +465,7 @@ func (c *compilerContext) renderFrom(sel *qcode.Select) { } if sel.Ti.Type == "function" { - c.renderFunctionTable(sel) + c.renderTableFunction(sel) return } @@ -480,28 +486,6 @@ func (c *compilerContext) renderFrom(sel *qcode.Select) { } } -func (c *compilerContext) renderFunctionTable(sel *qcode.Select) { - c.quoted(sel.Table) - c.w.WriteString(`(`) - i := 0 - for j, v := range sel.Args { - if v.Name != "args" { - continue - } - if i != 0 { - c.w.WriteString(`, `) - } - if v.Type == qcode.ArgTypeVar { - c.renderParam(Param{Name: v.Val, Type: sel.Ti.Args[j].Type}) - } else { - c.w.WriteString(v.Val) - } - i++ - } - c.w.WriteString(`) AS`) - c.quoted(sel.Table) -} - func (c *compilerContext) renderFromCursor(sel *qcode.Select) { if sel.Paging.Cursor { c.w.WriteString(`, __cur`) diff --git a/core/internal/psql/recur.go b/core/internal/psql/recur.go index 2e91c988..9138518d 100644 --- a/core/internal/psql/recur.go +++ b/core/internal/psql/recur.go @@ -77,7 +77,7 @@ func (c *compilerContext) renderRecursiveColumns(sel *qcode.Select) { c.w.WriteString(` THEN `) } if f.Type == qcode.FieldTypeFunc { - c.renderFunction(sel, f) + c.renderFieldFunction(sel, f) } else { c.colWithTable(f.Col.Table, f.Col.Name) } diff --git a/core/internal/qcode/fields.go b/core/internal/qcode/fields.go index ae8fd2ef..e31b593e 100644 --- a/core/internal/qcode/fields.go +++ b/core/internal/qcode/fields.go @@ -79,6 +79,15 @@ func (co *Compiler) compileChildColumns( return err } + switch { + case f.Name == "__typename": + sel.Typename = true + continue + + case strings.HasSuffix(f.Name, "_cursor"): + continue + } + fn, isFunc, err := co.isFunction(sel, f) if err != nil { return err @@ -89,18 +98,14 @@ func (co *Compiler) compileChildColumns( field.Func = fn.Func field.Args = fn.Args - // if err := co.compileFuncArgs(&field, f.Args); err != nil { - // return err - // } + if err := co.compileFuncArgs(&field, f.Args); err != nil { + return err + } if fn.Agg && sel.Rel.Type == sdata.RelRecursive { sel.addBaseCol(Column{Col: fn.Args[0].Col}) } - // for aggregate functions add column to group by - // if fn.Agg && len(fn.Args) == 1 { - // sel.addGroupCol(fn.Args[0].Col) - // } aggExists = fn.Agg } else { // not a function @@ -124,33 +129,62 @@ func (co *Compiler) compileChildColumns( return nil } -// func (co *Compiler) compileFuncArgs(f *Field, args []graph.Arg) error { -// if len(args) != 0 && len(f.Func.Inputs) == 0 { -// return fmt.Errorf("db function '%s' does not have any arguments", f.Func.Name) -// } - -// for _, arg := range args { -// _, err := f.Func.GetInput(arg.Name) -// if err != nil { -// return fmt.Errorf("db function %s: %w", f.Func.Name, err) -// } -// a := Arg{ -// Name: arg.Name, -// Val: arg.Val.Val, -// } -// if arg.Val.Type == graph.NodeVar { -// a.Type = ArgTypeVar -// } -// // if arg.Val.Type = graph. -// // fn.Col, err = sel.Ti.GetColumn(fname[(len(fn.Name) + 1):]) -// // if err != nil { -// // return -// // } -// f.Args = append(f.Args, a) -// } - -// return nil -// } +func (co *Compiler) compileFuncArgs(f *Field, args []graph.Arg) error { + if len(args) != 0 && len(f.Func.Inputs) == 0 { + return fmt.Errorf("db function '%s' does not have any arguments", f.Func.Name) + } + + for _, arg := range args { + if arg.Name == "args" { + if err := co.compileFuncArgArgs(f, arg); err != nil { + return err + } + continue + } + input, err := f.Func.GetInput(arg.Name) + if err != nil { + return fmt.Errorf("db function %s: %w", f.Func.Name, err) + } + a := Arg{ + Name: arg.Name, + Val: arg.Val.Val, + ValType: input.Type, + } + if arg.Val.Type == graph.NodeVar { + a.Type = ArgTypeVar + } + // if arg.Val.Type = graph. + // fn.Col, err = sel.Ti.GetColumn(fname[(len(fn.Name) + 1):]) + // if err != nil { + // return + // } + f.Args = append(f.Args, a) + } + + return nil +} + +func (co *Compiler) compileFuncArgArgs(f *Field, arg graph.Arg) error { + if len(f.Func.Inputs) == 0 { + return fmt.Errorf("db function '%s' does not have any arguments", f.Func.Name) + } + + node := arg.Val + + if node.Type != graph.NodeList { + return argErr("args", "list") + } + + for i, n := range node.Children { + a := Arg{Val: n.Val, ValType: f.Func.Inputs[i].Type} + if n.Type == graph.NodeVar { + a.Type = ArgTypeVar + } + f.Args = append(f.Args, a) + } + + return nil +} func (co *Compiler) addOrderByColumns(sel *Select) { for _, ob := range sel.OrderBy { diff --git a/core/internal/qcode/fn.go b/core/internal/qcode/fn.go index cb638504..afa0c02a 100644 --- a/core/internal/qcode/fn.go +++ b/core/internal/qcode/fn.go @@ -17,7 +17,7 @@ func (co *Compiler) isFunction(sel *Select, f graph.Field) ( switch { case f.Name == "search_rank": isFunc = true - if _, ok := sel.GetArg("search"); !ok { + if _, ok := sel.GetInternalArg("search"); !ok { err = fmt.Errorf("no search defined: %s", f.Name) } @@ -29,15 +29,10 @@ func (co *Compiler) isFunction(sel *Select, f graph.Field) ( if err != nil { return } - if _, ok := sel.GetArg("search"); !ok { + if _, ok := sel.GetInternalArg("search"); !ok { err = fmt.Errorf("no search defined: %s", f.Name) } - case f.Name == "__typename": - sel.Typename = true - - case strings.HasSuffix(f.Name, "_cursor"): - default: var fi funcInfo if fi, isFunc, err = co.isFunctionEx(sel, f); isFunc { diff --git a/core/internal/qcode/qcode.go b/core/internal/qcode/qcode.go index c642ad23..7818805b 100644 --- a/core/internal/qcode/qcode.go +++ b/core/internal/qcode/qcode.go @@ -74,6 +74,7 @@ type QCode struct { Script string Cache Cache Validation *Validation + Typename bool } type Select struct { @@ -87,6 +88,7 @@ type Select struct { FieldName string Fields []Field BCols []Column + IArgs []Arg Args []Arg Funcs []Function Where Filter @@ -189,10 +191,11 @@ const ( ) type Arg struct { - Type ArgType - Name string - Val string - Col sdata.DBColumn + Type ArgType + Name string + Val string + ValType string + Col sdata.DBColumn } type OrderBy struct { @@ -401,6 +404,9 @@ func (co *Compiler) compileQuery(qc *QCode, op *graph.Operation, role string) er for _, f := range op.Fields { if f.ParentID == -1 { + if f.Name == "__typename" && op.Name != "" { + qc.Typename = true + } val := f.ID | (-1 << 16) st.Push(val) } @@ -659,7 +665,7 @@ func (co *Compiler) setRelFilters(qc *QCode, sel *Select) { ex2 := newExp() ex3 := newExp() - v, _ := sel.GetArg("find") + v, _ := sel.GetInternalArg("find") switch v.Val { case "parents", "parent": ex1.Left.Table = rcte @@ -1056,7 +1062,7 @@ func (co *Compiler) compileArgs(sel *Select, args []graph.Arg, role string) erro func (co *Compiler) validateSelect(sel *Select) error { if sel.Rel.Type == sdata.RelRecursive { - v, ok := sel.GetArg("find") + v, ok := sel.GetInternalArg("find") if !ok { return fmt.Errorf("argument 'find' needed for recursive queries") } @@ -1412,7 +1418,7 @@ func (co *Compiler) compileArgFind(sel *Select, arg *graph.Arg) error { if arg.Val.Val != "parents" && arg.Val.Val != "children" { return fmt.Errorf("valid values 'parents' or 'children'") } - sel.addArg(Arg{Name: arg.Name, Val: arg.Val.Val}) + sel.addIArg(Arg{Name: arg.Name, Val: arg.Val.Val}) return nil } @@ -1477,7 +1483,7 @@ func (co *Compiler) compileArgSearch(sel *Select, arg *graph.Arg) error { ex.Right.ValType = ValVar ex.Right.Val = arg.Val.Val - sel.addArg(Arg{Name: arg.Name, Val: arg.Val.Val}) + sel.addIArg(Arg{Name: arg.Name, Val: arg.Val.Val}) setFilter(&sel.Where, ex) return nil } @@ -1681,20 +1687,12 @@ func (co *Compiler) compileArgArgs(sel *Select, arg *graph.Arg) error { return argErr("args", "list") } - if len(node.Children) != len(sel.Ti.Args) { - return fmt.Errorf("db function '%s' expects %d value(s) you have provided %d", - sel.Ti.Name, len(sel.Ti.Args), len(node.Children)) - } - - for _, n := range node.Children { - arg := Arg{ - Name: arg.Name, - Val: n.Val, - } + for i, n := range node.Children { + a := Arg{Val: n.Val, ValType: sel.Ti.Args[i].Type} if n.Type == graph.NodeVar { - arg.Type = ArgTypeVar + a.Type = ArgTypeVar } - sel.addArg(arg) + sel.Args = append(sel.Args, a) } return nil @@ -1970,13 +1968,13 @@ func dbArgErr(name, ty, db string) error { return fmt.Errorf("%s: value for argument '%s' must be a %s", db, name, ty) } -func (sel *Select) addArg(arg Arg) { - sel.Args = append(sel.Args, arg) +func (sel *Select) addIArg(arg Arg) { + sel.IArgs = append(sel.IArgs, arg) } -func (sel *Select) GetArg(name string) (Arg, bool) { +func (sel *Select) GetInternalArg(name string) (Arg, bool) { var arg Arg - for _, v := range sel.Args { + for _, v := range sel.IArgs { if v.Name == name { return v, true } diff --git a/core/query_pg_test.go b/core/query_pg_test.go index 334ab968..61f959c5 100644 --- a/core/query_pg_test.go +++ b/core/query_pg_test.go @@ -13,30 +13,55 @@ import ( "github.com/stretchr/testify/assert" ) -// func Example_queryWithFunctionFields() { -// gql := ` -// query { -// products(id: 51) { -// id -// name -// is_hot_product(id: 51) -// } -// }` - -// conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) -// gj, err := core.NewGraphJin(conf, db) -// if err != nil { -// panic(err) -// } - -// res, err := gj.GraphQL(context.Background(), gql, nil, nil) -// if err != nil { -// fmt.Println(err) -// } else { -// printJSON(res.Data) -// } -// // Output: {"products":[{"id":1,"name":"Product 1"},{"id":2,"name":"Product 2"}],"users":[]} -// } +func Example_queryWithFunctionFields() { + gql := ` + query { + products(id: 51) { + id + name + is_hot_product(id: 51) + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + res, err := gj.GraphQL(context.Background(), gql, nil, nil) + if err != nil { + fmt.Println(err) + } else { + printJSON(res.Data) + } + // Output: {"products":{"id":51,"is_hot_product":true,"name":"Product 51"}} +} + +func Example_queryWithFunctionFieldsArgList() { + gql := ` + query { + products(id: 51) { + id + name + is_hot_product(args: [51]) + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + res, err := gj.GraphQL(context.Background(), gql, nil, nil) + if err != nil { + fmt.Println(err) + } else { + printJSON(res.Data) + } + // Output: {"products":{"id":51,"is_hot_product":true,"name":"Product 51"}} +} func Example_queryWithVariableLimit() { gql := `query { diff --git a/core/query_test.go b/core/query_test.go index 9886a975..b32d2a72 100644 --- a/core/query_test.go +++ b/core/query_test.go @@ -1503,3 +1503,28 @@ func Example_queryWithWhereHasAnyKey() { // Output: {"products":[{"id":1},{"id":2},{"id":3}]} } + +func Example_queryWithTypename() { + gql := `query getUser { + __typename + users(id: 1) { + id + email + __typename + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + res, err := gj.GraphQL(context.Background(), gql, json.RawMessage(`{"id":2}`), nil) + if err != nil { + fmt.Println(err) + } else { + printJSON(res.Data) + } + // Output: {"__typename":"getUser","users":{"__typename":"users","email":"user1@test.com","id":1}} +}