feat: add some initial introspection support. (#52)

This commit is contained in:
Hiram Chirino
2020-04-19 23:48:49 -04:00
committed by GitHub
parent 6f18d56ca0
commit 966aa9ce8c
9 changed files with 416 additions and 31 deletions

View File

@ -55,6 +55,7 @@ import (
_log "log"
"os"
"github.com/chirino/graphql"
"github.com/dosco/super-graph/core/internal/allow"
"github.com/dosco/super-graph/core/internal/crypto"
"github.com/dosco/super-graph/core/internal/psql"
@ -92,6 +93,7 @@ type SuperGraph struct {
anonExists bool
qc *qcode.Compiler
pc *psql.Compiler
Engine *graphql.Engine
}
// NewSuperGraph creates the SuperGraph struct, this involves querying the database to learn its
@ -123,6 +125,10 @@ func NewSuperGraph(conf *Config, db *sql.DB) (*SuperGraph, error) {
return nil, err
}
if err := sg.initGraphQLEgine(); err != nil {
return nil, err
}
if len(conf.SecretKey) != 0 {
sk := sha256.Sum256([]byte(conf.SecretKey))
conf.SecretKey = ""
@ -154,6 +160,14 @@ type Result struct {
// In developer mode all names queries are saved into a file `allow.list` and in production mode only
// queries from this file can be run.
func (sg *SuperGraph) GraphQL(c context.Context, query string, vars json.RawMessage) (*Result, error) {
// try to use the sg.Engine to execute introspection queries...
res := sg.Engine.ExecuteOne(&graphql.EngineRequest{ Query: query})
if res.Error()==nil {
r := &Result{}
r.Data = res.Data
return r, nil
}
ct := scontext{Context: c, sg: sg, query: query, vars: vars}
if len(vars) <= 2 {

174
core/graph-schema.go Normal file
View File

@ -0,0 +1,174 @@
package core
import (
"strings"
"github.com/chirino/graphql"
"github.com/chirino/graphql/resolvers"
"github.com/chirino/graphql/schema"
"github.com/dosco/super-graph/core/internal/psql"
)
var typeMap map[string]string = map[string]string{
"smallint": "Int",
"integer": "Int",
"bigint": "Int",
"smallserial": "Int",
"serial": "Int",
"bigserial": "Int",
"decimal": "Float",
"numeric": "Float",
"real": "Float",
"double precision": "Float",
"money": "Float",
"boolean": "Boolean",
}
func (sg *SuperGraph) initGraphQLEgine() error {
sg.Engine = graphql.New()
engineSchema := sg.Engine.Schema
dbSchema := sg.schema
sanitizeForGraphQLSchema := func(value string) string {
// TODO:
return value
}
gqltype := func(col psql.DBColumn) schema.Type {
typeName := typeMap[strings.ToLower(col.Type)]
if typeName == "" {
typeName = "String"
}
var t schema.Type = &schema.TypeName{Ident: schema.Ident{Text: typeName}}
if col.NotNull {
t = &schema.NonNull{OfType: t}
}
return t
}
query := &schema.Object{
Name: "Query",
Fields: schema.FieldList{},
}
mutation := &schema.Object{
Name: "Mutation",
Fields: schema.FieldList{},
}
engineSchema.Types[query.Name] = query
engineSchema.Types[mutation.Name] = mutation
engineSchema.EntryPoints[schema.Query] = query
engineSchema.EntryPoints[schema.Mutation] = mutation
tableNames := dbSchema.GetTableNames()
for _, table := range tableNames {
ti, err := dbSchema.GetTable(table)
if err != nil {
return err
}
if !ti.IsSingular {
continue
}
pti, err := dbSchema.GetTable(ti.Plural)
if err != nil {
return err
}
outputType := &schema.Object{
Name: sanitizeForGraphQLSchema(ti.Singular) + "Output",
Fields: schema.FieldList{},
}
inputType := &schema.InputObject{
Name: sanitizeForGraphQLSchema(ti.Singular) + "Input",
Fields: schema.InputValueList{},
}
for _, col := range ti.Columns {
outputType.Fields = append(outputType.Fields, &schema.Field{
Name: sanitizeForGraphQLSchema(col.Name),
Type: gqltype(col),
})
inputType.Fields = append(inputType.Fields, &schema.InputValue{
Name: schema.Ident{Text: sanitizeForGraphQLSchema(col.Name)},
Type: gqltype(col),
})
}
engineSchema.Types[outputType.Name] = outputType
engineSchema.Types[inputType.Name] = inputType
outputTypeName := &schema.TypeName{Ident: schema.Ident{Text: outputType.Name}}
inputTypeName := &schema.TypeName{Ident: schema.Ident{Text: inputType.Name}}
pluralOutputTypeName := &schema.NonNull{OfType: &schema.List{OfType: &schema.NonNull{OfType: &schema.TypeName{Ident: schema.Ident{Text: outputType.Name}}}}}
pluralInputTypeName := &schema.NonNull{OfType: &schema.List{OfType: &schema.NonNull{OfType: &schema.TypeName{Ident: schema.Ident{Text: inputType.Name}}}}}
args := schema.InputValueList{}
if ti.PrimaryCol != nil {
t := gqltype(*ti.PrimaryCol)
if _, ok := t.(*schema.NonNull); !ok {
t = &schema.NonNull{OfType: t}
}
arg := &schema.InputValue{
Name: schema.Ident{Text: "id"},
Type: t,
}
args = append(args, arg)
}
query.Fields = append(query.Fields, &schema.Field{
Name: sanitizeForGraphQLSchema(ti.Singular),
Type: outputTypeName,
Args: args,
})
query.Fields = append(query.Fields, &schema.Field{
Name: sanitizeForGraphQLSchema(pti.Plural),
Type: pluralOutputTypeName,
Args: args,
})
mutation.Fields = append(mutation.Fields, &schema.Field{
Name: sanitizeForGraphQLSchema(ti.Singular),
Args: append(args, schema.InputValueList{
&schema.InputValue{
Name: schema.Ident{Text: "insert"},
Type: inputTypeName,
},
&schema.InputValue{
Name: schema.Ident{Text: "inserts"},
Type: pluralInputTypeName,
},
&schema.InputValue{
Name: schema.Ident{Text: "update"},
Type: inputTypeName,
},
&schema.InputValue{
Name: schema.Ident{Text: "updates"},
Type: pluralInputTypeName,
},
}...),
Type: outputType,
})
}
err := engineSchema.ResolveTypes()
if err != nil {
return err
}
sg.Engine.Resolver = resolvers.Func(func(request *resolvers.ResolveRequest, next resolvers.Resolution) resolvers.Resolution {
resolver := resolvers.MetadataResolver.Resolve(request, next)
if resolver != nil {
return resolver
}
resolver = resolvers.MethodResolver.Resolve(request, next) // needed by the MetadataResolver
if resolver != nil {
return resolver
}
return nil
})
return nil
}

View File

@ -7,6 +7,7 @@ import (
"testing"
"github.com/dosco/super-graph/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -90,6 +91,15 @@ func TestSuperGraph(t *testing.T, db *sql.DB, before func(t *testing.T)) {
require.Equal(t, `{"line_items": [{"id": 5001}, {"id": 5002}]}`, string(res.Data))
})
t.Run("get line item", func(t *testing.T) {
before(t)
res, err := sg.GraphQL(ctx,
`query { line_item(id:$id) { id, price, quantity } }`,
json.RawMessage(`{"id":5001}`))
require.NoError(t, err, res.SQL())
require.Equal(t, `{"line_item": {"id": 5001, "price": 6.95, "quantity": 10}}`, string(res.Data))
})
t.Run("get line items", func(t *testing.T) {
before(t)
res, err := sg.GraphQL(ctx,
@ -140,4 +150,154 @@ func TestSuperGraph(t *testing.T, db *sql.DB, before func(t *testing.T)) {
require.Equal(t, `{"line_items": [{"id": 5003, "product": {"name": "Charmin Ultra Soft"}}]}`, string(res.Data))
})
t.Run("schema introspection", func(t *testing.T) {
before(t)
// fmt.Println(sg.Engine.Schema.String())
assert.Equal(t, `type Mutation {
line_item(id:Int!, insert:line_itemInput, inserts:[line_itemInput!]!, update:line_itemInput, updates:[line_itemInput!]!):line_itemOutput
product(id:Int!, insert:productInput, inserts:[productInput!]!, update:productInput, updates:[productInput!]!):productOutput
user(id:Int!, insert:userInput, inserts:[userInput!]!, update:userInput, updates:[userInput!]!):userOutput
}
type Query {
line_item(id:Int!):line_itemOutput
line_items(id:Int!):[line_itemOutput!]!
product(id:Int!):productOutput
products(id:Int!):[productOutput!]!
user(id:Int!):userOutput
users(id:Int!):[userOutput!]!
}
input line_itemInput {
id:Int!
price:Float
product:Int
quantity:Int
}
type line_itemOutput {
id:Int!
price:Float
product:Int
quantity:Int
}
input productInput {
id:Int!
name:String
weight:Float
}
type productOutput {
id:Int!
name:String
weight:Float
}
input userInput {
full_name:String
id:Int!
}
type userOutput {
full_name:String
id:Int!
}
schema {
mutation: Mutation
query: Query
}
`, sg.Engine.Schema.String())
})
res, err := sg.GraphQL(ctx,introspectionQuery, json.RawMessage(``))
assert.NoError(t, err)
assert.Contains(t, string(res.Data),
`{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":null,"types":`)
assert.Contains(t, string(res.Data),
`{"kind":"OBJECT","name":"Mutation","description":null,"fields":[{"name":"line_item","description":null`)
}
const introspectionQuery = `
query {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
`

View File

@ -141,7 +141,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (
c.renderLateralJoin(sel)
}
if !ti.Singular {
if !ti.IsSingular {
c.renderPluralSelect(sel, ti)
}
@ -178,7 +178,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (
io.WriteString(c.w, `)`)
aliasWithID(c.w, "__sj", sel.ID)
if !ti.Singular {
if !ti.IsSingular {
io.WriteString(c.w, `)`)
aliasWithID(c.w, "__sj", sel.ID)
}
@ -706,7 +706,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
}
switch {
case ti.Singular:
case ti.IsSingular:
io.WriteString(c.w, ` LIMIT ('1') :: integer`)
case len(sel.Paging.Limit) != 0:

View File

@ -16,12 +16,14 @@ type DBSchema struct {
type DBTableInfo struct {
Name string
Type string
Singular bool
IsSingular bool
Columns []DBColumn
PrimaryCol *DBColumn
TSVCol *DBColumn
ColMap map[string]*DBColumn
ColIDMap map[int16]*DBColumn
Singular string
Plural string
}
type RelType int
@ -89,23 +91,28 @@ func (s *DBSchema) addTable(
colidmap := make(map[int16]*DBColumn, len(cols))
singular := flect.Singularize(t.Key)
plural := flect.Pluralize(t.Key)
s.t[singular] = &DBTableInfo{
Name: t.Name,
Type: t.Type,
Singular: true,
Columns: cols,
ColMap: colmap,
ColIDMap: colidmap,
Name: t.Name,
Type: t.Type,
IsSingular: true,
Columns: cols,
ColMap: colmap,
ColIDMap: colidmap,
Singular: singular,
Plural: plural,
}
plural := flect.Pluralize(t.Key)
s.t[plural] = &DBTableInfo{
Name: t.Name,
Type: t.Type,
Singular: false,
Columns: cols,
ColMap: colmap,
ColIDMap: colidmap,
Name: t.Name,
Type: t.Type,
IsSingular: false,
Columns: cols,
ColMap: colmap,
ColIDMap: colidmap,
Singular: singular,
Plural: plural,
}
if al, ok := aliases[t.Key]; ok {
@ -364,6 +371,14 @@ func (s *DBSchema) updateSchemaOTMT(
return nil
}
func (s *DBSchema) GetTableNames() []string {
var names []string
for name, _ := range s.t {
names = append(names, name)
}
return names
}
func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {
t, ok := s.t[table]
if !ok {