Skip to content

Commit

Permalink
feat: rewrite of the introspection subsystem
Browse files Browse the repository at this point in the history
- replaced the chirino library with our own implementation for greater flexiblity
- we now generated exact and detailed types for everything, tables, columns, functions, roles, etc
- focus on better client generation and ide autocomplete
- new config param enable_introspection to write introspection json to a file (intro.json)
- [breaking] changed db schema (sdl) file name from `db.schema` to `db.graphql`
- major code cleanup of directives and arguments subsystem
- added two new field level arguments `includeIf` and `skipIf` to skip and include fields using a filter expression
- arguments used instead of directives to maintain type safety as directives cannot have field specific types
- added two new field level directives `@add` and `@remove` to add and remove fields based on current role
- all functions must now use `args` field argument for their own arguments
- positional arguments must use the key `a0, a1, a2, etc`
  • Loading branch information
dosco committed Jan 15, 2023
1 parent 91722f9 commit db68640
Show file tree
Hide file tree
Showing 37 changed files with 1,810 additions and 1,723 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ RUN yarn
RUN yarn build

# stage: 2
FROM golang:1.19 as go-build
FROM golang:1.20rc3 as go-build
RUN go install github.com/rafaelsq/wtc@latest

WORKDIR /app
Expand Down
14 changes: 11 additions & 3 deletions core/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ const (
UserRoleKey
)

const (
APQ_PX = "_apq"
)

// GraphJin struct is an instance of the GraphJin engine it holds all the required information like
// datase schemas, relationships, etc that the GraphQL to SQL compiler would need to do it's job.
type graphjin struct {
Expand Down Expand Up @@ -293,7 +297,7 @@ func (g *GraphJin) GraphQL(c context.Context,

// get query from apq cache if apq key exists
if rc != nil && rc.APQKey != "" {
queryBytes, inCache = gj.cache.Get(rc.APQKey)
queryBytes, inCache = gj.cache.Get(APQ_PX + rc.APQKey)
}

// query not found in apq cache so use original query
Expand Down Expand Up @@ -328,7 +332,7 @@ func (g *GraphJin) GraphQL(c context.Context,

// save to apq cache is apq key exists and not already in cache
if !inCache && rc != nil && rc.APQKey != "" {
gj.cache.Set(rc.APQKey, r.query)
gj.cache.Set((APQ_PX + rc.APQKey), r.query)
}

// if not production then save to allow list
Expand Down Expand Up @@ -473,8 +477,12 @@ func (gj *graphjin) query(c context.Context, r graphqlReq) (

// Reload redoes database discover and reinitializes GraphJin.
func (g *GraphJin) Reload() error {
return g.reload(nil)
}

func (g *GraphJin) reload(di *sdata.DBInfo) error {
gj := g.Load().(*graphjin)
gjNew, err := newGraphJin(gj.conf, gj.db, nil, gj.fs, gj.opts...)
gjNew, err := newGraphJin(gj.conf, gj.db, di, gj.fs, gj.opts...)
if err == nil {
g.Store(gjNew)
}
Expand Down
14 changes: 10 additions & 4 deletions core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ type Config struct {
// in production mode.
EnableSchema bool `mapstructure:"enable_schema" json:"enable_schema" yaml:"enable_schema" jsonschema:"title=Enable Schema,default=false"`

// When set to true an introspection json file will be generated in dev mode.
// This file can be used with other GraphQL tooling to generate clients, enable
// autocomplete, etc
EnableIntrospection bool `mapstructure:"enable_introspection" json:"enable_introspection" yaml:"enable_introspection" jsonschema:"title=Generate introspection JSON,default=false"`

// Forces the database session variable 'user.id' to be set to the user id
SetUserID bool `mapstructure:"set_user_id" json:"set_user_id" yaml:"set_user_id" jsonschema:"title=Set User ID,default=false"`

Expand Down Expand Up @@ -123,10 +128,11 @@ type Column struct {

// Configuration for user role
type Role struct {
Name string
Match string `jsonschema:"title=Related To,example=other_table.id_column,example=users.id"`
Tables []RoleTable `jsonschema:"title=Table Configuration for Role"`
tm map[string]*RoleTable
Name string
Comment string
Match string `jsonschema:"title=Related To,example=other_table.id_column,example=users.id"`
Tables []RoleTable `jsonschema:"title=Table Configuration for Role"`
tm map[string]*RoleTable
}

// Table configuration for a specific role (user role)
Expand Down
82 changes: 43 additions & 39 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,14 @@ func (gj *graphjin) getIntroResult() (data json.RawMessage, err error) {
if data, ok = gj.cache.Get("_intro"); ok {
return
}
in := newIntro(gj.schema, gj.conf.EnableCamelcase)
if data, err = introspection(in); err != nil {
if data, err = gj.introQuery(); err != nil {
return
}
gj.cache.Set("_intro", data)
return
}

func (gj *graphjin) initDiscover() error {
func (gj *graphjin) initDiscover() (err error) {
switch gj.conf.DBType {
case "":
gj.dbtype = "postgres"
Expand All @@ -77,16 +76,15 @@ func (gj *graphjin) initDiscover() error {
gj.dbtype = gj.conf.DBType
}

if err := gj._initDiscover(); err != nil {
return fmt.Errorf("%s: %w", gj.dbtype, err)
if err = gj._initDiscover(); err != nil {
err = fmt.Errorf("%s: %w", gj.dbtype, err)
}

return nil
return
}

func (gj *graphjin) _initDiscover() (err error) {
if gj.prod && gj.conf.EnableSchema {
b, err := gj.fs.ReadFile("db.schema")
b, err := gj.fs.ReadFile("db.graphql")
if err != nil {
return err
}
Expand All @@ -103,30 +101,29 @@ func (gj *graphjin) _initDiscover() (err error) {
gj.conf.Blocklist)
}

// If gj.dbinfo is not null then it's probably set
// for tests or the schema file is being used
if gj.dbinfo != nil {
return
}

gj.dbinfo, err = sdata.GetDBInfo(
gj.db,
gj.dbtype,
gj.conf.Blocklist)
if err != nil {
return
// gj.dbinfo could be preset due to tests or db
// watcher reloading
if gj.dbinfo == nil {
gj.dbinfo, err = sdata.GetDBInfo(
gj.db,
gj.dbtype,
gj.conf.Blocklist)
if err != nil {
return
}
}

if !gj.prod && gj.conf.EnableSchema {
var buf bytes.Buffer
if err := writeSchema(gj.dbinfo, &buf); err != nil {
return err
}
err = gj.fs.CreateFile("db.schema", buf.Bytes())
err = gj.fs.CreateFile("db.graphql", buf.Bytes())
if err != nil {
return
}
}

return
}

Expand All @@ -137,9 +134,7 @@ func (gj *graphjin) initSchema() error {
return nil
}

func (gj *graphjin) _initSchema() error {
var err error

func (gj *graphjin) _initSchema() (err error) {
if len(gj.dbinfo.Tables) == 0 {
return fmt.Errorf("no tables found in database")
}
Expand All @@ -154,33 +149,42 @@ func (gj *graphjin) _initSchema() error {
if t.Table != "" && t.Type == "" {
continue
}
if err := addTableInfo(gj.conf, t); err != nil {
return err
if err = addTableInfo(gj.conf, t); err != nil {
return
}
}

if err := addTables(gj.conf, gj.dbinfo); err != nil {
return err
if err = addTables(gj.conf, gj.dbinfo); err != nil {
return
}

if err := addForeignKeys(gj.conf, gj.dbinfo); err != nil {
return err
if err = addForeignKeys(gj.conf, gj.dbinfo); err != nil {
return
}

gj.schema, err = sdata.NewDBSchema(
gj.dbinfo,
getDBTableAliases(gj.conf))

if err != nil {
return err
return
}

return err
if !gj.prod && gj.conf.EnableIntrospection {
var introJSON json.RawMessage
introJSON, err = gj.getIntroResult()
if err != nil {
return
}
err = gj.fs.CreateFile("intro.json", []byte(introJSON))
if err != nil {
return
}
}
return
}

func (gj *graphjin) initCompilers() error {
var err error

func (gj *graphjin) initCompilers() (err error) {
qcc := qcode.Config{
TConfig: gj.conf.tmap,
DefaultBlock: gj.conf.DefaultBlock,
Expand All @@ -193,11 +197,11 @@ func (gj *graphjin) initCompilers() error {

gj.qc, err = qcode.NewCompiler(gj.schema, qcc)
if err != nil {
return err
return
}

if err := addRoles(gj.conf, gj.qc); err != nil {
return err
if err = addRoles(gj.conf, gj.qc); err != nil {
return
}

gj.pc = psql.NewCompiler(psql.Config{
Expand All @@ -207,7 +211,7 @@ func (gj *graphjin) initCompilers() error {
SecPrefix: gj.pf,
EnableCamelcase: gj.conf.EnableCamelcase,
})
return nil
return
}

func (gj *graphjin) executeRoleQuery(c context.Context,
Expand Down
38 changes: 24 additions & 14 deletions core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"testing"
"time"

core "github.com/dosco/graphjin/v2/core"
"github.com/dosco/graphjin/v2/core/internal/allow"
Expand Down Expand Up @@ -223,25 +224,37 @@ func TestEnableSchema(t *testing.T) {
}`

dir, err := os.MkdirTemp("", "test")
assert.NoError(t, err)
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)

fs := fs.NewOsFSWithBase(dir)

conf1 := newConfig(&core.Config{DBType: dbType, EnableSchema: true})
gj1, err := core.NewGraphJin(conf1, db, core.OptionSetFS(fs))
assert.NoError(t, err)
if err != nil {
panic(err)
}

res1, err := gj1.GraphQL(context.Background(), gql, nil, nil)
assert.NoError(t, err)
if err != nil {
panic(err)
}
assert.Equal(t, stdJSON(res1.Data), `{"products":{"id":2,"name":"Product 2"}}`)

time.Sleep(3 * time.Second)

conf2 := newConfig(&core.Config{DBType: dbType, EnableSchema: true, Production: true})
gj2, err := core.NewGraphJin(conf2, db, core.OptionSetFS(fs))
assert.NoError(t, err)
if err != nil {
panic(err)
}

res2, err := gj2.GraphQL(context.Background(), gql, nil, nil)
assert.NoError(t, err)
if err != nil {
panic(err)
}
assert.Equal(t, stdJSON(res2.Data), `{"products":{"id":2,"name":"Product 2"}}`)
}

Expand All @@ -256,27 +269,24 @@ func TestConfigReuse(t *testing.T) {
for i := 0; i < 50; i++ {
gj1, err := core.NewGraphJin(conf, db)
if err != nil {
t.Error(err)
panic(err)
}

res1, err := gj1.GraphQL(context.Background(), gql, nil, nil)
if err != nil {
t.Error(err)
panic(err)
}

gj2, err := core.NewGraphJin(conf, db)
if err != nil {
t.Error(err)
panic(err)
}

res2, err := gj2.GraphQL(context.Background(), gql, nil, nil)
if err != nil {
t.Error(err)
panic(err)
}

assert.Equal(t, res1.Data, res2.Data, "should equal")
}

}

func TestConfigRoleManagement(t *testing.T) {
Expand Down Expand Up @@ -412,13 +422,13 @@ func BenchmarkCompile(b *testing.B) {

gj, err := core.NewGraphJin(conf, db)
if err != nil {
b.Error(err)
panic(err)
}

for n := 0; n < b.N; n++ {
res, err := gj.GraphQL(context.Background(), benchGQL, vars, nil)
if err != nil {
b.Fatal(err)
panic(err)
}

resultJSON = res.Data
Expand Down
9 changes: 3 additions & 6 deletions core/internal/graph/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func (p *Parser) parseOp() (op Operation, err error) {
return op, nil
}

func (p *Parser) parseOpTypeAndArgs(op *Operation) error {
func (p *Parser) parseOpTypeAndArgs(op *Operation) (err error) {
item := p.next()

switch {
Expand All @@ -286,8 +286,6 @@ func (p *Parser) parseOpTypeAndArgs(op *Operation) error {

op.Args = op.argsA[:0]

var err error

if p.peek(itemName) {
op.Name = p.val(p.next())
}
Expand All @@ -307,8 +305,7 @@ func (p *Parser) parseOpTypeAndArgs(op *Operation) error {
return err
}
}

return nil
return
}

func ParseArgValue(argVal string, json bool) (*Node, error) {
Expand Down Expand Up @@ -381,7 +378,7 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {

func (p *Parser) parseNormalFields(st *Stack, fields []Field) ([]Field, error) {
if !p.peek(itemName) {
return nil, fmt.Errorf("expecting an alias or field name, got: %s", p.next())
return nil, p.tokErr(`expecting an alias or field name`)
}

fields = append(fields, Field{ID: int32(len(fields))})
Expand Down
Loading

1 comment on commit db68640

@vercel
Copy link

@vercel vercel bot commented on db68640 Jan 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.