Compare commits

...

3 Commits

20 changed files with 118 additions and 66 deletions

View File

@ -5,11 +5,11 @@ web_ui: true
# debug, info, warn, error, fatal, panic
log_level: "debug"
# Disable this in development to get a list of
# queries used. When enabled super graph
# will only allow queries from this list
# List saved to ./config/allow.list
use_allow_list: false
# When production mode is 'true' only queries
# from the allow list are permitted.
# When it's 'false' all queries are saved to the
# the allow list in ./config/allow.list
production: true
# Throw a 401 on auth failure for queries that need auth
auth_fail_block: false

View File

@ -9,11 +9,11 @@ web_ui: false
# debug, info, warn, error, fatal, panic, disable
log_level: "info"
# Disable this in development to get a list of
# queries used. When enabled super graph
# will only allow queries from this list
# List saved to ./config/allow.list
use_allow_list: true
# When production mode is 'true' only queries
# from the allow list are permitted.
# When it's 'false' all queries are saved to the
# the allow list in ./config/allow.list
production: true
# Throw a 401 on auth failure for queries that need auth
auth_fail_block: true

View File

@ -24,6 +24,12 @@
:item="actionLink"
/>
<a
class="px-4 py-3 my-8 border-2 border-gray-500 text-gray-600 font-bold rounded"
href="https://github.com/dosco/super-graph"
target="_blank"
>Github</a>
</div>
</div>

View File

@ -1,6 +1,11 @@
let ogprefix = 'og: http://ogp.me/ns#'
let title = 'Super Graph'
let description = 'An instant GraphQL API for your app. No code needed.'
let color = '#f42525'
module.exports = {
title: 'Super Graph',
description: 'Get an instant GraphQL API for your Rails apps.',
title: title,
description: description,
themeConfig: {
logo: '/hologram.svg',
@ -15,6 +20,22 @@ module.exports = {
serviceWorker: {
updatePopup: true
},
head: [
//['link', { rel: 'icon', href: `/assets/favicon.ico` }],
['meta', { prefix: ogprefix, property: 'og:title', content: title }],
['meta', { prefix: ogprefix, property: 'twitter:title', content: title }],
['meta', { prefix: ogprefix, property: 'og:type', content: 'website' }],
['meta', { prefix: ogprefix, property: 'og:url', content: 'https://supergraph.dev }],
['meta', { prefix: ogprefix, property: 'og:description', content: description }],
//['meta', { prefix: ogprefix, property: 'og:image', content: 'https://wireupyourfrontend.com/assets/logo.png' }],
// ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
// ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
// ['link', { rel: 'apple-touch-icon', href: `/assets/apple-touch-icon.png` }],
// ['link', { rel: 'mask-icon', href: '/assets/safari-pinned-tab.svg', color: color }],
// ['meta', { name: 'msapplication-TileImage', content: '/assets/mstile-150x150.png' }],
// ['meta', { name: 'msapplication-TileColor', content: color }],
],
},
postcss: {

View File

@ -1149,11 +1149,11 @@ web_ui: true
# debug, info, warn, error, fatal, panic
log_level: "debug"
# Disable this in development to get a list of
# queries used. When enabled super graph
# will only allow queries from this list
# List saved to ./config/allow.list
use_allow_list: false
# When production mode is 'true' only queries
# from the allow list are permitted.
# When it's 'false' all queries are saved to the
# the allow list in ./config/allow.list
production: false
# Throw a 401 on auth failure for queries that need auth
auth_fail_block: false

View File

@ -16,7 +16,7 @@ import (
"github.com/pkg/errors"
)
var migrationPattern = regexp.MustCompile(`\A(\d+)_.+\.sql\z`)
var migrationPattern = regexp.MustCompile(`\A(\d+)_[^\.]+\.sql\z`)
var ErrNoFwMigration = errors.Errorf("no sql in forward migration step")
@ -127,7 +127,7 @@ func FindMigrationsEx(path string, fs MigratorFS) ([]string, error) {
return nil, err
}
mcount := len(paths) + 100
mcount := len(paths)
if n < int64(mcount) {
return nil, fmt.Errorf("Duplicate migration %d", n)

View File

@ -137,16 +137,23 @@ func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w io.Writer
}
for i := range root.PresetList {
cn := root.PresetList[i]
col, ok := ti.Columns[cn]
if !ok {
continue
}
if i != 0 {
io.WriteString(c.w, `, `)
}
if values {
io.WriteString(c.w, `'`)
io.WriteString(c.w, root.PresetMap[root.PresetList[i]])
io.WriteString(c.w, `'`)
io.WriteString(c.w, root.PresetMap[cn])
io.WriteString(c.w, `' :: `)
io.WriteString(c.w, col.Type)
} else {
io.WriteString(c.w, `"`)
io.WriteString(c.w, root.PresetList[i])
io.WriteString(c.w, cn)
io.WriteString(c.w, `"`)
}
}

View File

@ -250,7 +250,7 @@ func simpleInsertWithPresets(t *testing.T) {
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "name", "price", 'now', 'now', '$user_id' FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "name", "price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '$user_id' :: bigint FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`),
@ -273,7 +273,7 @@ func simpleUpdateWithPresets(t *testing.T) {
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "name", "price", 'now' FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = {{user_id}}) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "name", "price", 'now' :: timestamp without time zone FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = {{user_id}}) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),

View File

@ -191,15 +191,15 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
fallthrough
case RelBelongTo:
if _, ok := colmap[rel.Col2]; !ok {
cols = append(cols, &qcode.Column{ti.Name, rel.Col2, rel.Col2})
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Col2, FieldName: rel.Col2})
}
case RelOneToManyThrough:
if _, ok := colmap[rel.Col1]; !ok {
cols = append(cols, &qcode.Column{ti.Name, rel.Col1, rel.Col1})
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Col1, FieldName: rel.Col1})
}
case RelRemote:
if _, ok := colmap[rel.Col1]; !ok {
cols = append(cols, &qcode.Column{ti.Name, rel.Col1, rel.Col2})
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Col1, FieldName: rel.Col2})
}
skipped |= (1 << uint(id))
@ -340,9 +340,9 @@ func (c *compilerContext) renderLateralJoinClose(sel *qcode.Select) error {
return nil
}
func (c *compilerContext) renderJoin(sel *qcode.Select) error {
func (c *compilerContext) renderJoin(sel *qcode.Select, ti *DBTableInfo) error {
parent := &c.s[sel.ParentID]
return c.renderJoinByName(sel.Table, parent.Table, parent.ID)
return c.renderJoinByName(ti.Name, parent.Table, parent.ID)
}
func (c *compilerContext) renderJoinByName(table, parent string, id int32) error {
@ -607,7 +607,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
}
if !isRoot {
if err := c.renderJoin(sel); err != nil {
if err := c.renderJoin(sel, ti); err != nil {
return err
}
@ -691,7 +691,7 @@ func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInf
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
parent := c.s[sel.ParentID]
return c.renderRelationshipByName(sel.Table, parent.Table, parent.ID)
return c.renderRelationshipByName(ti.Name, parent.Table, parent.ID)
}
func (c *compilerContext) renderRelationshipByName(table, parent string, id int32) error {

2
qcode/cleanup.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
cd corpus && rm -rf $(find . ! -name '00?.gql')

View File

@ -577,7 +577,7 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) error {
case nodeVar:
ex.Type = ValVar
default:
fmt.Errorf("expecting a string, int, float or variable")
return fmt.Errorf("expecting a string, int, float or variable")
}
sel.Where = ex

View File

@ -71,7 +71,7 @@ func initAllowList(cpath string) {
}
if len(_allowList.filepath) == 0 {
if conf.UseAllowList {
if conf.Production {
logger.Fatal().Msg("allow.list not found")
}

View File

@ -124,7 +124,7 @@ func cmdDBNew(cmd *cobra.Command, args []string) {
os.Exit(1)
}
mname := fmt.Sprintf("%03d_%s.sql", len(m)+100, name)
mname := fmt.Sprintf("%03d_%s.sql", (len(m) + 1), name)
// Write new migration
mpath := filepath.Join(conf.MigrationsPath, mname)

View File

@ -21,7 +21,7 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
logger.Fatal().Err(err).Msg("failed to read config")
}
conf.UseAllowList = false
conf.Production = false
db, err = initDBPool(conf)
if err != nil {

View File

@ -23,6 +23,7 @@ type config struct {
LogLevel string `mapstructure:"log_level"`
EnableTracing bool `mapstructure:"enable_tracing"`
UseAllowList bool `mapstructure:"use_allow_list"`
Production bool
WatchAndReload bool `mapstructure:"reload_on_config_change"`
AuthFailBlock bool `mapstructure:"auth_fail_block"`
SeedFile string `mapstructure:"seed_file"`
@ -142,9 +143,10 @@ type configRoleTable struct {
}
type configRole struct {
Name string
Match string
Tables []configRoleTable
Name string
Match string
Tables []configRoleTable
tablesMap map[string]*configRoleTable
}
func newConfig(name string) *viper.Viper {
@ -195,6 +197,10 @@ func (c *config) Init(vi *viper.Viper) error {
c.Tables = c.DB.Tables
}
if c.UseAllowList {
c.Production = true
}
for k, v := range c.Inflections {
flect.AddPlural(k, v)
}
@ -219,13 +225,19 @@ func (c *config) Init(vi *viper.Viper) error {
rolesMap := make(map[string]struct{})
for i := range c.Roles {
role := c.Roles[i]
role := &c.Roles[i]
if _, ok := rolesMap[role.Name]; ok {
logger.Fatal().Msgf("duplicate role '%s' found", role.Name)
}
role.Name = sanitize(role.Name)
role.Match = sanitize(role.Match)
role.tablesMap = make(map[string]*configRoleTable)
for n, table := range role.Tables {
role.tablesMap[table.Name] = &role.Tables[n]
}
rolesMap[role.Name] = struct{}{}
}

View File

@ -54,7 +54,7 @@ func (c *coreContext) execQuery() ([]byte, error) {
logger.Debug().Str("role", c.req.role).Msg(c.req.Query)
if conf.UseAllowList {
if conf.Production {
var ps *preparedItem
data, ps, err = c.resolvePreparedSQL()
@ -256,7 +256,7 @@ func (c *coreContext) resolveSQL() ([]byte, uint32, error) {
stime)
}
if conf.UseAllowList == false {
if conf.Production == false {
_allowList.add(&c.req)
}
@ -325,7 +325,7 @@ func (c *coreContext) resolveRemote(
ob.WriteString("null")
}
to[0] = jsn.Field{[]byte(s.FieldName), ob.Bytes()}
to[0] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
return to, nil
}
@ -402,7 +402,7 @@ func (c *coreContext) resolveRemotes(
ob.WriteString("null")
}
to[n] = jsn.Field{[]byte(s.FieldName), ob.Bytes()}
to[n] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
}(i, id, s)
}
wg.Wait()

View File

@ -41,17 +41,22 @@ func (c *coreContext) buildStmt() ([]stmt, error) {
mutation := (qc.Type != qcode.QTQuery)
w := &bytes.Buffer{}
for i := range conf.Roles {
for i := 1; i < len(conf.Roles); i++ {
role := &conf.Roles[i]
// For mutations only render sql for a single role from the request
if mutation && len(c.req.role) != 0 && role.Name != c.req.role {
continue
}
if i > 0 {
qc, err = qcompile.Compile(gql, role.Name)
if err != nil {
return nil, err
qc, err = qcompile.Compile(gql, role.Name)
if err != nil {
return nil, err
}
if conf.Production && role.Name == "anon" {
if _, ok := role.tablesMap[qc.Selects[0].Table]; !ok {
continue
}
}

View File

@ -108,7 +108,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
// Ensure that we use the correct events, as they are not uniform across
// platforms. See https://github.com/fsnotify/fsnotify/issues/74
if conf.UseAllowList == false && strings.HasSuffix(event.Name, "/allow.list") {
if conf.Production == false && strings.HasSuffix(event.Name, "/allow.list") {
continue
}

View File

@ -1,15 +1,15 @@
app_name: "{{app_name}} Development"
app_name: "{% app_name %} Development"
host_port: 0.0.0.0:8080
web_ui: true
# debug, info, warn, error, fatal, panic
log_level: "debug"
# Disable this in development to get a list of
# queries used. When enabled super graph
# will only allow queries from this list
# List saved to ./config/allow.list
use_allow_list: false
# When production mode is 'true' only queries
# from the allow list are permitted.
# When it's 'false' all queries are saved to the
# the allow list in ./config/allow.list
production: false
# Throw a 401 on auth failure for queries that need auth
auth_fail_block: false
@ -48,7 +48,7 @@ migrations_path: ./config/migrations
auth:
# Can be 'rails' or 'jwt'
type: rails
cookie: _{{app_name_slug}}_session
cookie: _{% app_name_slug %}_session
# Comment this out if you want to disable setting
# the user_id via a header for testing.
@ -84,7 +84,7 @@ database:
type: postgres
host: db
port: 5432
dbname: {{app_name_slug}}_development
dbname: {% app_name_slug %}_development
user: postgres
password: ''

View File

@ -2,18 +2,17 @@
# so I only need to overwrite some values
inherits: dev
app_name: "{{app_name}} Production"
app_name: "{% app_name %} Production"
host_port: 0.0.0.0:8080
web_ui: false
# debug, info, warn, error, fatal, panic, disable
log_level: "info"
# Disable this in development to get a list of
# queries used. When enabled super graph
# will only allow queries from this list
# List saved to ./config/allow.list
use_allow_list: true
# When production mode is 'true' only queries
# from the allow list are permitted.
# When it's 'false' all queries are saved to the
# the allow list in ./config/allow.list
production: true
# Throw a 401 on auth failure for queries that need auth
auth_fail_block: true
@ -48,7 +47,7 @@ enable_tracing: true
auth:
# Can be 'rails' or 'jwt'
type: rails
cookie: _{{app_name_slug}}_session
cookie: _{% app_name_slug %}_session
rails:
# Rails version this is used for reading the
@ -79,7 +78,7 @@ database:
type: postgres
host: db
port: 5432
dbname: {{app_name_slug}}_development
dbname: {% app_name_slug %}_development
user: postgres
password: ''
#pool_size: 10