Add config driven custom table relationships
This commit is contained in:
@ -96,7 +96,7 @@ func initAllowList(cpath string) {
|
||||
}
|
||||
|
||||
func (al *allowList) add(req *gqlReq) {
|
||||
if len(req.ref) == 0 || len(req.Query) == 0 {
|
||||
if al.saveChan == nil || len(req.ref) == 0 || len(req.Query) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,10 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
|
||||
if len(fields) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
v := fields[0].Value
|
||||
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
||||
fields[0].Value = v[1 : len(v)-1]
|
||||
}
|
||||
|
||||
return w.Write(fields[0].Value)
|
||||
}
|
||||
|
@ -81,11 +81,17 @@ type config struct {
|
||||
roles map[string]*configRole
|
||||
}
|
||||
|
||||
type configColumn struct {
|
||||
Name string
|
||||
ForeignKey string `mapstructure:"related_to"`
|
||||
}
|
||||
|
||||
type configTable struct {
|
||||
Name string
|
||||
Table string
|
||||
Blocklist []string
|
||||
Remotes []configRemote
|
||||
Columns []configColumn
|
||||
}
|
||||
|
||||
type configRemote struct {
|
||||
@ -226,6 +232,7 @@ func (c *config) Init(vi *viper.Viper) error {
|
||||
if _, ok := c.roles[role.Name]; ok {
|
||||
errlog.Fatal().Msgf("duplicate role '%s' found", role.Name)
|
||||
}
|
||||
|
||||
role.Name = strings.ToLower(role.Name)
|
||||
role.Match = sanitize(role.Match)
|
||||
role.tablesMap = make(map[string]*configRoleTable)
|
||||
@ -296,6 +303,28 @@ func (c *config) getAliasMap() map[string][]string {
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *config) isABCLEnabled() bool {
|
||||
if len(c.RolesQuery) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch len(c.Roles) {
|
||||
case 0, 1:
|
||||
return false
|
||||
case 2:
|
||||
_, ok1 := c.roles["anon"]
|
||||
_, ok2 := c.roles["user"]
|
||||
return !(ok1 && ok2)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *config) isAnonRoleDefined() bool {
|
||||
_, ok := c.roles["anon"]
|
||||
return ok
|
||||
}
|
||||
|
||||
var varRe1 = regexp.MustCompile(`(?mi)\$([a-zA-Z0-9_.]+)`)
|
||||
var varRe2 = regexp.MustCompile(`\{\{([a-zA-Z0-9_.]+)\}\}`)
|
||||
|
||||
|
112
serv/config_compile.go
Normal file
112
serv/config_compile.go
Normal file
@ -0,0 +1,112 @@
|
||||
package serv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dosco/super-graph/psql"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
)
|
||||
|
||||
func addForeignKeys(c *config, di *psql.DBInfo) error {
|
||||
for _, t := range c.Tables {
|
||||
for _, c := range t.Columns {
|
||||
if err := addForeignKey(di, c, t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addForeignKey(di *psql.DBInfo, c configColumn, t configTable) error {
|
||||
c1, ok := di.GetColumn(t.Name, c.Name)
|
||||
if !ok {
|
||||
return fmt.Errorf(
|
||||
"Invalid table '%s' or column '%s in config",
|
||||
t.Name, c.Name)
|
||||
}
|
||||
|
||||
v := strings.SplitN(c.ForeignKey, ".", 2)
|
||||
if len(v) != 2 {
|
||||
return fmt.Errorf(
|
||||
"Invalid foreign_key in config for table '%s' and column '%s",
|
||||
t.Name, c.Name)
|
||||
}
|
||||
|
||||
fkt, fkc := v[0], v[1]
|
||||
c2, ok := di.GetColumn(fkt, fkc)
|
||||
if !ok {
|
||||
return fmt.Errorf(
|
||||
"Invalid foreign_key in config for table '%s' and column '%s",
|
||||
t.Name, c.Name)
|
||||
}
|
||||
|
||||
c1.FKeyTable = fkt
|
||||
c1.FKeyColID = []int16{c2.ID}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addRoles(c *config, qc *qcode.Compiler) error {
|
||||
for _, r := range c.Roles {
|
||||
for _, t := range r.Tables {
|
||||
if err := addRole(qc, r, t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addRole(qc *qcode.Compiler, r configRole, t configRoleTable) error {
|
||||
blockFilter := []string{"false"}
|
||||
|
||||
query := qcode.QueryConfig{
|
||||
Limit: t.Query.Limit,
|
||||
Filters: t.Query.Filters,
|
||||
Columns: t.Query.Columns,
|
||||
DisableFunctions: t.Query.DisableFunctions,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
query.Filters = blockFilter
|
||||
}
|
||||
|
||||
insert := qcode.InsertConfig{
|
||||
Filters: t.Insert.Filters,
|
||||
Columns: t.Insert.Columns,
|
||||
Presets: t.Insert.Presets,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
insert.Filters = blockFilter
|
||||
}
|
||||
|
||||
update := qcode.UpdateConfig{
|
||||
Filters: t.Insert.Filters,
|
||||
Columns: t.Insert.Columns,
|
||||
Presets: t.Insert.Presets,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
update.Filters = blockFilter
|
||||
}
|
||||
|
||||
delete := qcode.DeleteConfig{
|
||||
Filters: t.Insert.Filters,
|
||||
Columns: t.Insert.Columns,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
delete.Filters = blockFilter
|
||||
}
|
||||
|
||||
return qc.AddRole(r.Name, t.Name, qcode.TRConfig{
|
||||
Query: query,
|
||||
Insert: insert,
|
||||
Update: update,
|
||||
Delete: delete,
|
||||
})
|
||||
}
|
@ -77,7 +77,7 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *stmt, error) {
|
||||
mutation := (qt == qcode.QTMutation)
|
||||
anonQuery := (qt == qcode.QTQuery && c.req.role == "anon")
|
||||
|
||||
useRoleQuery := len(conf.RolesQuery) != 0 && mutation
|
||||
useRoleQuery := conf.isABCLEnabled() && mutation
|
||||
useTx := useRoleQuery || conf.DB.SetUserID
|
||||
|
||||
if useTx {
|
||||
@ -127,7 +127,7 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *stmt, error) {
|
||||
row = db.QueryRow(c.Context, ps.sd.SQL, vars...)
|
||||
}
|
||||
|
||||
if mutation || anonQuery {
|
||||
if mutation || anonQuery || !conf.isABCLEnabled() {
|
||||
err = row.Scan(&root)
|
||||
} else {
|
||||
err = row.Scan(&role, &root)
|
||||
|
@ -24,14 +24,16 @@ func buildStmt(qt qcode.QType, gql, vars []byte, role string) ([]stmt, error) {
|
||||
return buildRoleStmt(gql, vars, role)
|
||||
|
||||
case qcode.QTQuery:
|
||||
switch {
|
||||
case role == "anon":
|
||||
return buildRoleStmt(gql, vars, role)
|
||||
if role == "anon" {
|
||||
return buildRoleStmt(gql, vars, "anon")
|
||||
}
|
||||
|
||||
default:
|
||||
if conf.isABCLEnabled() {
|
||||
return buildMultiStmt(gql, vars)
|
||||
}
|
||||
|
||||
return buildRoleStmt(gql, vars, "user")
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown query type '%d'", qt)
|
||||
}
|
||||
|
@ -77,7 +77,15 @@ func prepareStmt(gql string, vars []byte) error {
|
||||
|
||||
switch qt {
|
||||
case qcode.QTQuery:
|
||||
stmts1, err := buildMultiStmt(q, vars)
|
||||
var stmts1 []stmt
|
||||
var err error
|
||||
|
||||
if conf.isABCLEnabled() {
|
||||
stmts1, err = buildMultiStmt(q, vars)
|
||||
} else {
|
||||
stmts1, err = buildRoleStmt(q, vars, "user")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -87,14 +95,16 @@ func prepareStmt(gql string, vars []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
stmts2, err := buildRoleStmt(q, vars, "anon")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if conf.isAnonRoleDefined() {
|
||||
stmts2, err := buildRoleStmt(q, vars, "anon")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = prepare(tx, &stmts2[0], gqlHash(gql, vars, "anon"))
|
||||
if err != nil {
|
||||
return err
|
||||
err = prepare(tx, &stmts2[0], gqlHash(gql, vars, "anon"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case qcode.QTMutation:
|
||||
@ -142,7 +152,7 @@ func prepare(tx pgx.Tx, st *stmt, key string) error {
|
||||
|
||||
// nolint: errcheck
|
||||
func prepareRoleStmt(tx pgx.Tx) error {
|
||||
if len(conf.RolesQuery) == 0 {
|
||||
if !conf.isABCLEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
12
serv/reso.go
12
serv/reso.go
@ -36,7 +36,6 @@ func initResolvers() error {
|
||||
|
||||
func initRemotes(t configTable) error {
|
||||
h := xxhash.New()
|
||||
var err error
|
||||
|
||||
for _, r := range t.Remotes {
|
||||
// defines the table column to be used as an id in the
|
||||
@ -46,21 +45,20 @@ func initRemotes(t configTable) error {
|
||||
// if no table column specified in the config then
|
||||
// use the primary key of the table as the id
|
||||
if len(idcol) == 0 {
|
||||
idcol, err = pcompile.IDColumn(t.Name)
|
||||
pcol, err := pcompile.IDColumn(t.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idcol = pcol.Key
|
||||
}
|
||||
idk := fmt.Sprintf("__%s_%s", t.Name, idcol)
|
||||
|
||||
// register a relationship between the remote data
|
||||
// and the database table
|
||||
|
||||
val := &psql.DBRel{
|
||||
Type: psql.RelRemote,
|
||||
Col1: idcol,
|
||||
Col2: idk,
|
||||
}
|
||||
val := &psql.DBRel{Type: psql.RelRemote}
|
||||
val.Left.Col = idcol
|
||||
val.Right.Col = idk
|
||||
|
||||
err := pcompile.AddRelationship(strings.ToLower(r.Name), t.Name, val)
|
||||
if err != nil {
|
||||
|
76
serv/serv.go
76
serv/serv.go
@ -15,77 +15,29 @@ import (
|
||||
)
|
||||
|
||||
func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
||||
var err error
|
||||
|
||||
schema, err = psql.NewDBSchema(db, c.getAliasMap())
|
||||
di, err := psql.GetDBInfo(db)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
conf := qcode.Config{
|
||||
if err = addForeignKeys(c, di); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
schema, err = psql.NewDBSchema(db, di, c.getAliasMap())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
qc, err := qcode.NewCompiler(qcode.Config{
|
||||
Blocklist: c.DB.Blocklist,
|
||||
}
|
||||
|
||||
qc, err := qcode.NewCompiler(conf)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
blockFilter := []string{"false"}
|
||||
|
||||
for _, r := range c.Roles {
|
||||
for _, t := range r.Tables {
|
||||
query := qcode.QueryConfig{
|
||||
Limit: t.Query.Limit,
|
||||
Filters: t.Query.Filters,
|
||||
Columns: t.Query.Columns,
|
||||
DisableFunctions: t.Query.DisableFunctions,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
query.Filters = blockFilter
|
||||
}
|
||||
|
||||
insert := qcode.InsertConfig{
|
||||
Filters: t.Insert.Filters,
|
||||
Columns: t.Insert.Columns,
|
||||
Presets: t.Insert.Presets,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
insert.Filters = blockFilter
|
||||
}
|
||||
|
||||
update := qcode.UpdateConfig{
|
||||
Filters: t.Insert.Filters,
|
||||
Columns: t.Insert.Columns,
|
||||
Presets: t.Insert.Presets,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
update.Filters = blockFilter
|
||||
}
|
||||
|
||||
delete := qcode.DeleteConfig{
|
||||
Filters: t.Insert.Filters,
|
||||
Columns: t.Insert.Columns,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
delete.Filters = blockFilter
|
||||
}
|
||||
|
||||
err := qc.AddRole(r.Name, t.Name, qcode.TRConfig{
|
||||
Query: query,
|
||||
Insert: insert,
|
||||
Update: update,
|
||||
Delete: delete,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if err := addRoles(c, qc); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pc := psql.NewCompiler(psql.Config{
|
||||
|
Reference in New Issue
Block a user