Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
62fd1eac55 | |||
1a3d74e1ce | |||
3a4d885987 | |||
3bd9b199dd | |||
4ffa1483a4 | |||
52f3b1c7a2 | |||
2d466bfb12 | |||
a0b8907c3c | |||
8097ca3b8f | |||
0e498b0e94 | |||
3eb5b83070 | |||
e3c94d17d1 | |||
7240b27214 | |||
f37d867e32 | |||
5e75cc7b83 | |||
d4dca86267 | |||
76340ab008 | |||
3f5727c22b |
2
.gitignore
vendored
2
.gitignore
vendored
@ -35,3 +35,5 @@ supergraph
|
|||||||
crashers
|
crashers
|
||||||
suppressions
|
suppressions
|
||||||
release
|
release
|
||||||
|
.gofuzz
|
||||||
|
*-fuzz.zip
|
||||||
|
337
allow/allow.go
Normal file
337
allow/allow.go
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
package allow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AL_QUERY int = iota + 1
|
||||||
|
AL_VARS
|
||||||
|
)
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Name string
|
||||||
|
key string
|
||||||
|
URI string
|
||||||
|
Query string
|
||||||
|
Vars json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
type List struct {
|
||||||
|
filepath string
|
||||||
|
saveChan chan Item
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
CreateIfNotExists bool
|
||||||
|
Persist bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cpath string, conf Config) (*List, error) {
|
||||||
|
al := List{}
|
||||||
|
|
||||||
|
if len(cpath) != 0 {
|
||||||
|
fp := path.Join(cpath, "allow.list")
|
||||||
|
|
||||||
|
if _, err := os.Stat(fp); err == nil {
|
||||||
|
al.filepath = fp
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(al.filepath) == 0 {
|
||||||
|
fp := "./allow.list"
|
||||||
|
|
||||||
|
if _, err := os.Stat(fp); err == nil {
|
||||||
|
al.filepath = fp
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(al.filepath) == 0 {
|
||||||
|
fp := "./config/allow.list"
|
||||||
|
|
||||||
|
if _, err := os.Stat(fp); err == nil {
|
||||||
|
al.filepath = fp
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(al.filepath) == 0 {
|
||||||
|
if !conf.CreateIfNotExists {
|
||||||
|
return nil, errors.New("allow.list not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cpath) == 0 {
|
||||||
|
al.filepath = "./config/allow.list"
|
||||||
|
} else {
|
||||||
|
al.filepath = path.Join(cpath, "allow.list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if conf.Persist {
|
||||||
|
al.saveChan = make(chan Item)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for v := range al.saveChan {
|
||||||
|
if err = al.save(v); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &al, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *List) IsPersist() bool {
|
||||||
|
return al.saveChan != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *List) Add(vars []byte, query, uri string) error {
|
||||||
|
if al.saveChan == nil {
|
||||||
|
return errors.New("allow.list is read-only")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query) == 0 {
|
||||||
|
return errors.New("empty query")
|
||||||
|
}
|
||||||
|
|
||||||
|
var q string
|
||||||
|
|
||||||
|
for i := 0; i < len(query); i++ {
|
||||||
|
c := query[i]
|
||||||
|
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' {
|
||||||
|
q = query
|
||||||
|
break
|
||||||
|
|
||||||
|
} else if c == '{' {
|
||||||
|
q = "query " + query
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
al.saveChan <- Item{
|
||||||
|
URI: uri,
|
||||||
|
Query: q,
|
||||||
|
Vars: vars,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *List) Load() ([]Item, error) {
|
||||||
|
var list []Item
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(al.filepath)
|
||||||
|
if err != nil {
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) == 0 {
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri string
|
||||||
|
var varBytes []byte
|
||||||
|
|
||||||
|
itemMap := make(map[string]struct{})
|
||||||
|
|
||||||
|
s, e, c := 0, 0, 0
|
||||||
|
ty := 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
fq := false
|
||||||
|
|
||||||
|
if c == 0 && b[e] == '#' {
|
||||||
|
s = e
|
||||||
|
for e < len(b) && b[e] != '\n' {
|
||||||
|
e++
|
||||||
|
}
|
||||||
|
if (e - s) > 2 {
|
||||||
|
uri = strings.TrimSpace(string(b[(s + 1):e]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e >= len(b) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchPrefix(b, e, "query") || matchPrefix(b, e, "mutation") {
|
||||||
|
if c == 0 {
|
||||||
|
s = e
|
||||||
|
}
|
||||||
|
ty = AL_QUERY
|
||||||
|
} else if matchPrefix(b, e, "variables") {
|
||||||
|
if c == 0 {
|
||||||
|
s = e + len("variables") + 1
|
||||||
|
}
|
||||||
|
ty = AL_VARS
|
||||||
|
} else if b[e] == '{' {
|
||||||
|
c++
|
||||||
|
|
||||||
|
} else if b[e] == '}' {
|
||||||
|
c--
|
||||||
|
|
||||||
|
if c == 0 {
|
||||||
|
if ty == AL_QUERY {
|
||||||
|
fq = true
|
||||||
|
} else if ty == AL_VARS {
|
||||||
|
varBytes = b[s:(e + 1)]
|
||||||
|
}
|
||||||
|
ty = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fq {
|
||||||
|
query := string(b[s:(e + 1)])
|
||||||
|
name := QueryName(query)
|
||||||
|
key := strings.ToLower(name)
|
||||||
|
|
||||||
|
if _, ok := itemMap[key]; !ok {
|
||||||
|
v := Item{
|
||||||
|
Name: name,
|
||||||
|
key: key,
|
||||||
|
URI: uri,
|
||||||
|
Query: query,
|
||||||
|
Vars: varBytes,
|
||||||
|
}
|
||||||
|
list = append(list, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
varBytes = nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
e++
|
||||||
|
if e >= len(b) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *List) save(item Item) error {
|
||||||
|
item.Name = QueryName(item.Query)
|
||||||
|
item.key = strings.ToLower(item.Name)
|
||||||
|
|
||||||
|
if len(item.Name) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := al.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
index := -1
|
||||||
|
|
||||||
|
for i, v := range list {
|
||||||
|
if strings.EqualFold(v.Name, item.Name) {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if index != -1 {
|
||||||
|
list[index] = item
|
||||||
|
} else {
|
||||||
|
list = append(list, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(al.filepath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
return strings.Compare(list[i].key, list[j].key) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, v := range list {
|
||||||
|
_, err := f.WriteString(fmt.Sprintf("# %s\n\n", v.URI))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.Vars) != 0 && !bytes.Equal(v.Vars, []byte("{}")) {
|
||||||
|
vj, err := json.MarshalIndent(v.Vars, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal vars: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Query[0] == '{' {
|
||||||
|
_, err = f.WriteString(fmt.Sprintf("query %s\n\n", v.Query))
|
||||||
|
} else {
|
||||||
|
_, err = f.WriteString(fmt.Sprintf("%s\n\n", v.Query))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchPrefix(b []byte, i int, s string) bool {
|
||||||
|
if (len(b) - i) < len(s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for n := 0; n < len(s); n++ {
|
||||||
|
if b[(i+n)] != s[n] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryName(b string) string {
|
||||||
|
state, s := 0, 0
|
||||||
|
|
||||||
|
for i := 0; i < len(b); i++ {
|
||||||
|
switch {
|
||||||
|
case state == 2 && b[i] == '{':
|
||||||
|
return b[s:i]
|
||||||
|
case state == 2 && b[i] == ' ':
|
||||||
|
return b[s:i]
|
||||||
|
case state == 1 && b[i] == '{':
|
||||||
|
return ""
|
||||||
|
case state == 1 && b[i] != ' ':
|
||||||
|
s = i
|
||||||
|
state = 2
|
||||||
|
case state == 1 && b[i] == ' ':
|
||||||
|
continue
|
||||||
|
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
|
||||||
|
state = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
82
allow/allow_test.go
Normal file
82
allow/allow_test.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package allow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGQLName1(t *testing.T) {
|
||||||
|
var q = `
|
||||||
|
query {
|
||||||
|
products(
|
||||||
|
distinct: [price]
|
||||||
|
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
|
||||||
|
) { id name } }`
|
||||||
|
|
||||||
|
name := QueryName(q)
|
||||||
|
|
||||||
|
if len(name) != 0 {
|
||||||
|
t.Fatal("Name should be empty, not ", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGQLName2(t *testing.T) {
|
||||||
|
var q = `
|
||||||
|
query hakuna_matata {
|
||||||
|
products(
|
||||||
|
distinct: [price]
|
||||||
|
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
name := QueryName(q)
|
||||||
|
|
||||||
|
if name != "hakuna_matata" {
|
||||||
|
t.Fatal("Name should be 'hakuna_matata', not ", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGQLName3(t *testing.T) {
|
||||||
|
var q = `
|
||||||
|
mutation means{ users { id } }`
|
||||||
|
|
||||||
|
// var v2 = ` { products( limit: 30, order_by: { price: desc }, distinct: [ price ] where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) { id name price user { id email } } } `
|
||||||
|
|
||||||
|
name := QueryName(q)
|
||||||
|
|
||||||
|
if name != "means" {
|
||||||
|
t.Fatal("Name should be 'means', not ", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGQLName4(t *testing.T) {
|
||||||
|
var q = `
|
||||||
|
query no_worries
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
name := QueryName(q)
|
||||||
|
|
||||||
|
if name != "no_worries" {
|
||||||
|
t.Fatal("Name should be 'no_worries', not ", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGQLName5(t *testing.T) {
|
||||||
|
var q = `
|
||||||
|
{
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
name := QueryName(q)
|
||||||
|
|
||||||
|
if len(name) != 0 {
|
||||||
|
t.Fatal("Name should be empty, not ", name)
|
||||||
|
}
|
||||||
|
}
|
15
allow/fuzz_test.go
Normal file
15
allow/fuzz_test.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package allow
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFuzzCrashers(t *testing.T) {
|
||||||
|
var crashers = []string{
|
||||||
|
"query",
|
||||||
|
"q",
|
||||||
|
"que",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range crashers {
|
||||||
|
_ = QueryName(f)
|
||||||
|
}
|
||||||
|
}
|
@ -167,10 +167,13 @@ roles:
|
|||||||
block: false
|
block: false
|
||||||
|
|
||||||
- name: deals
|
- name: deals
|
||||||
|
|
||||||
query:
|
query:
|
||||||
limit: 3
|
limit: 3
|
||||||
columns: ["name", "description" ]
|
aggregation: false
|
||||||
|
|
||||||
|
- name: purchases
|
||||||
|
query:
|
||||||
|
limit: 3
|
||||||
aggregation: false
|
aggregation: false
|
||||||
|
|
||||||
- name: user
|
- name: user
|
||||||
@ -183,12 +186,10 @@ roles:
|
|||||||
query:
|
query:
|
||||||
limit: 50
|
limit: 50
|
||||||
filters: ["{ user_id: { eq: $user_id } }"]
|
filters: ["{ user_id: { eq: $user_id } }"]
|
||||||
columns: ["id", "name", "description", "search_rank", "search_headline_description" ]
|
|
||||||
disable_functions: false
|
disable_functions: false
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
filters: ["{ user_id: { eq: $user_id } }"]
|
filters: ["{ user_id: { eq: $user_id } }"]
|
||||||
columns: ["id", "name", "description" ]
|
|
||||||
presets:
|
presets:
|
||||||
- user_id: "$user_id"
|
- user_id: "$user_id"
|
||||||
- created_at: "now"
|
- created_at: "now"
|
||||||
|
@ -137,7 +137,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<div class="text-2xl md:text-3xl">
|
<div class="text-2xl md:text-3xl">
|
||||||
<small class="text-sm">Download the Docker compose config for the demo</small>
|
<small class="text-sm">Download the Docker compose config for the demo</small>
|
||||||
<pre>‣ curl -L -o demo.yml https://bit.ly/2mq05lW</pre>
|
<pre>‣ curl -L -o demo.yml https://bit.ly/2FZS0uw</pre>
|
||||||
|
|
||||||
<small class="text-sm">Setup the demo database</small>
|
<small class="text-sm">Setup the demo database</small>
|
||||||
<pre>‣ docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed</pre>
|
<pre>‣ docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed</pre>
|
||||||
|
395
docs/guide.md
395
docs/guide.md
@ -4,9 +4,9 @@ sidebar: auto
|
|||||||
|
|
||||||
# Guide to Super Graph
|
# Guide to Super Graph
|
||||||
|
|
||||||
Super Graph is a micro-service that instantly and without code gives you a high performance and secure GraphQL API. Your GraphQL queries are auto translated into a single fast SQL query. No more writing API code as you develop your web frontend just make the query you need and Super Graph will do the rest.
|
Super Graph is a service that instantly and without code gives you a high performance and secure GraphQL API. Your GraphQL queries are auto translated into a single fast SQL query. No more spending weeks or months writing backend API code. Just make the query you need and Super Graph will do the rest.
|
||||||
|
|
||||||
Super Graph has a rich feature set like integrating with your existing Ruby on Rails apps, joining your DB with data from remote APIs, Role and Attribute based access control, Supoport for JWT tokens, DB migrations, seeding and a lot more.
|
Super Graph has a rich feature set like integrating with your existing Ruby on Rails apps, joining your DB with data from remote APIs, Role and Attribute based access control, Support for JWT tokens, DB migrations, seeding and a lot more.
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@ -31,14 +31,14 @@ Super Graph has a rich feature set like integrating with your existing Ruby on R
|
|||||||
## Try the demo app
|
## Try the demo app
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# download the Docker compose config for the demo
|
# clone the repository
|
||||||
curl -L -o demo.yml https://bit.ly/2mq05lW
|
git clone https://github.com/dosco/super-graph
|
||||||
|
|
||||||
# setup the demo rails app & database and run it
|
# setup the demo rails app & database and run it
|
||||||
docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed
|
docker-compose run rails_app rake db:create db:migrate db:seed
|
||||||
|
|
||||||
# run the demo
|
# run the demo
|
||||||
docker-compose -f demo.yml up
|
docker-compose up
|
||||||
|
|
||||||
# signin to the demo app (user1@demo.com / 123456)
|
# signin to the demo app (user1@demo.com / 123456)
|
||||||
open http://localhost:3000
|
open http://localhost:3000
|
||||||
@ -47,14 +47,14 @@ open http://localhost:3000
|
|||||||
open http://localhost:8080
|
open http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
::: warning DEMO REQUIREMENTS
|
::: tip DEMO REQUIREMENTS
|
||||||
This demo requires `docker` you can either install it using `brew` or from the
|
This demo requires `docker` you can either install it using `brew` or from the
|
||||||
docker website [https://docs.docker.com/docker-for-mac/install/](https://docs.docker.com/docker-for-mac/install/)
|
docker website [https://docs.docker.com/docker-for-mac/install/](https://docs.docker.com/docker-for-mac/install/)
|
||||||
:::
|
:::
|
||||||
|
|
||||||
#### Trying out GraphQL
|
#### Trying out GraphQL
|
||||||
|
|
||||||
We currently fully support queries and mutations. Support for `subscriptions` is work in progress. For example the below GraphQL query would fetch two products that belong to the current user where the price is greater than 10.
|
We fully support queries and mutations. For example the below GraphQL query would fetch two products that belong to the current user where the price is greater than 10.
|
||||||
|
|
||||||
#### GQL Query
|
#### GQL Query
|
||||||
|
|
||||||
@ -76,32 +76,6 @@ query {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In another example the below GraphQL mutation would insert a product into the database. The first part of the below example is the variable data and the second half is the GraphQL mutation. For mutations data has to always ben passed as a variable.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"name": "Art of Computer Programming",
|
|
||||||
"description": "The Art of Computer Programming (TAOCP) is a comprehensive monograph written by computer scientist Donald Knuth",
|
|
||||||
"price": 30.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
mutation {
|
|
||||||
product(insert: $data) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The above GraphQL query returns the JSON result below. It handles all
|
|
||||||
kinds of complexity without you having to writing a line of code.
|
|
||||||
|
|
||||||
For example there is a while greater than `gt` and a limit clause on a child field. And the `avatar` field is renamed to `picture`. The `password` field is blocked and not returned. Finally the relationship between the `users` table and the `products` table is auto discovered and used.
|
|
||||||
|
|
||||||
#### JSON Result
|
#### JSON Result
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -128,19 +102,107 @@ For example there is a while greater than `gt` and a limit clause on a child fie
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Try with an authenticated user
|
::: tip Testing with a user
|
||||||
|
In development mode you can use the `X-User-ID: 4` header to set a user id so you don't have to worries about cookies etc. This can be set using the *HTTP Headers* tab at the bottom of the web UI.
|
||||||
|
:::
|
||||||
|
|
||||||
In development mode you can use the `X-User-ID: 4` header to set a user id so you don't have to worries about cookies etc. This can be set using the *HTTP Headers* tab at the bottom of the web UI you'll see when you visit the above link. You can also directly run queries from the commandline like below.
|
In another example the below GraphQL mutation would insert a product into the database. The first part of the below example is the variable data and the second half is the GraphQL mutation. For mutations data has to always ben passed as a variable.
|
||||||
|
|
||||||
#### Querying the GQL endpoint
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"name": "Art of Computer Programming",
|
||||||
|
"description": "The Art of Computer Programming (TAOCP) is a comprehensive monograph written by computer scientist Donald Knuth",
|
||||||
|
"price": 30.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```graphql
|
||||||
|
mutation {
|
||||||
|
product(insert: $data) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# fetch the response json directly from the endpoint using user id 5
|
## Why Super Graph
|
||||||
curl 'http://localhost:8080/api/v1/graphql' \
|
|
||||||
-H 'content-type: application/json' \
|
Let's take a simple example say you want to fetch 5 products priced over 12 dollars along with the photos of the products and the users that owns them. Additionally also fetch the last 10 of your own purchases along with the name and ID of the product you purchased. This is a common type of query to render a view in say an ecommerce app. Lets be honest it's not very exciting write and maintain. Keep in mind the data needed will only continue to grow and change as your app evolves. Developers might find that most ORMs will not be able to do all of this in a single SQL query and will require n+1 queries to fetch all the data and assembly it into the right JSON response.
|
||||||
-H 'X-User-ID: 5' \
|
|
||||||
--data-binary '{"query":"{ products { name price users { email }}}"}'
|
What if I told you Super Graph will fetch all this data with a single SQL query and without you having to write a single line of code. Also as your app evolves feel free to evolve the query as you like. In our experience Super Graph saves us hundreds or thousands of man hours that we can put towards the more exciting parts of our app.
|
||||||
|
|
||||||
|
#### GraphQL Query
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products(limit 5, where: { price: { gt: 12 } }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
price
|
||||||
|
photos {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
picture : avatar
|
||||||
|
full_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
purchases(
|
||||||
|
limit 10,
|
||||||
|
order_by: { created_at: desc } ,
|
||||||
|
where: { user_id: { eq: $user_id } }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
product {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSON Result
|
||||||
|
|
||||||
|
```json
|
||||||
|
|
||||||
|
"data": {
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Oaked Arrogant Bastard Ale",
|
||||||
|
"description": "Coors lite, European Amber Lager, Perle, 1272 - American Ale II, 38 IBU, 6.4%, 9.7°Blg",
|
||||||
|
"price": 20,
|
||||||
|
"photos: [{
|
||||||
|
"url": "https://www.scienceworld.ca/wp-content/uploads/science-world-beer-flavours.jpg"
|
||||||
|
}],
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"email": "user0@demo.com",
|
||||||
|
"picture": "https://robohash.org/sitaliquamquaerat.png?size=300x300&set=set1",
|
||||||
|
"full_name": "Mrs. Wilhemina Hilpert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"purchases": [
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"created_at": "2020-01-24T05:34:39.880599",
|
||||||
|
"product": {
|
||||||
|
"id": 45,
|
||||||
|
"name": "Brooklyn Black",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
@ -651,8 +713,6 @@ query {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mutations
|
|
||||||
|
|
||||||
In GraphQL mutations is the operation type for when you need to modify data. Super Graph supports the `insert`, `update`, `upsert` and `delete`. You can also do complex nested inserts and updates.
|
In GraphQL mutations is the operation type for when you need to modify data. Super Graph supports the `insert`, `update`, `upsert` and `delete`. You can also do complex nested inserts and updates.
|
||||||
|
|
||||||
When using mutations the data must be passed as variables since Super Graphs compiles the query into an prepared statement in the database for maximum speed. Prepared statements are are functions in your code when called they accept arguments and your variables are passed in as those arguments.
|
When using mutations the data must be passed as variables since Super Graphs compiles the query into an prepared statement in the database for maximum speed. Prepared statements are are functions in your code when called they accept arguments and your variables are passed in as those arguments.
|
||||||
@ -836,8 +896,6 @@ mutation {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Nested Mutations
|
|
||||||
|
|
||||||
Often you will need to create or update multiple related items at the same time. This can be done using nested mutations. For example you might need to create a product and assign it to a user, or create a user and his products at the same time. You just have to use simple json to define you mutation and Super Graph takes care of the rest.
|
Often you will need to create or update multiple related items at the same time. This can be done using nested mutations. For example you might need to create a product and assign it to a user, or create a user and his products at the same time. You just have to use simple json to define you mutation and Super Graph takes care of the rest.
|
||||||
|
|
||||||
### Nested Insert
|
### Nested Insert
|
||||||
@ -906,7 +964,7 @@ mutation {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Nested Updates
|
### Nested Update
|
||||||
|
|
||||||
Update a product item first and then assign it to a user
|
Update a product item first and then assign it to a user
|
||||||
|
|
||||||
@ -966,7 +1024,7 @@ mutation {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using variables
|
## Using Variables
|
||||||
|
|
||||||
Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The build-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner
|
Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The build-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner
|
||||||
and better client side code.
|
and better client side code.
|
||||||
@ -988,6 +1046,104 @@ fetch('http://localhost:8080/api/v1/graphql', {
|
|||||||
.then(res => console.log(res.data));
|
.then(res => console.log(res.data));
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## GraphQL with React
|
||||||
|
|
||||||
|
This is a quick simple example using `graphql.js` [https://github.com/f/graphql.js/](https://github.com/f/graphql.js/)
|
||||||
|
|
||||||
|
```js
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import graphql from 'graphql.js'
|
||||||
|
|
||||||
|
// Create a GraphQL client pointing to Super Graph
|
||||||
|
var graph = graphql("http://localhost:3000/api/v1/graphql", { asJSON: true })
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [user, setUser] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function action() {
|
||||||
|
// Use the GraphQL client to execute a graphQL query
|
||||||
|
// The second argument to the client are the variables you need to pass
|
||||||
|
const result = await graph(`{ user { id first_name last_name picture_url } }`)()
|
||||||
|
setUser(result)
|
||||||
|
}
|
||||||
|
action()
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<h1>{ JSON.stringify(user) }</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
## Advanced Columns
|
||||||
|
|
||||||
|
The ablity to have `JSON/JSONB` and `Array` columns is often considered in the top most useful features of Postgres. There are many cases where using an array or a json column saves space and reduces complexity in your app. The only issue with these columns is the really that your SQL queries can get harder to write and maintain.
|
||||||
|
|
||||||
|
Super Graph steps in here to help you by supporting these columns right out of the box. It allows you to work with these columns just like you would with tables. Joining data against or modifying array columns using the `connect` or `disconnect` keywords in mutations is fully supported. Another very useful feature is the ability to treat `json` or `binary json (jsonb)` columns as seperate tables, even using them in nested queries joining against related tables. To replicate these features on your own will take a lot of complex SQL. Using Super Graph means you don't have to deal with any of this it just works.
|
||||||
|
|
||||||
|
### Array Columns
|
||||||
|
|
||||||
|
Configure a relationship between an array column `tag_ids` which contains integer id's for tags and the column `id` in the table `tags`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tables:
|
||||||
|
- name: posts
|
||||||
|
columns:
|
||||||
|
- name: tag_ids
|
||||||
|
related_to: tags.id
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
posts {
|
||||||
|
title
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Column
|
||||||
|
|
||||||
|
Configure a JSON column called `tag_count` in the table `products` into a seperate table. This JSON column contains a json array of objects each with a tag id and a count of the number of times the tag was used. As a seperate table you can nest it into your GraphQL query and treat it like table using any of the standard features like `order_by`, `limit`, `where clauses`, etc.
|
||||||
|
|
||||||
|
The configuration below tells Super Graph to create a synthetic table called `tag_count` using the column `tag_count` from the `products` table. And that this new table has two columns `tag_id` and `count` of the listed types and with the defined relationships.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tables:
|
||||||
|
- name: tag_count
|
||||||
|
table: products
|
||||||
|
columns:
|
||||||
|
- name: tag_id
|
||||||
|
type: bigint
|
||||||
|
related_to: tags.id
|
||||||
|
- name: count
|
||||||
|
type: int
|
||||||
|
```
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
products {
|
||||||
|
name
|
||||||
|
tag_counts {
|
||||||
|
count
|
||||||
|
tag {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Full text search
|
## Full text search
|
||||||
|
|
||||||
Every app these days needs search. Enought his often means reaching for something heavy like Solr. While this will work why add complexity to your infrastructure when Postgres has really great
|
Every app these days needs search. Enought his often means reaching for something heavy like Solr. While this will work why add complexity to your infrastructure when Postgres has really great
|
||||||
@ -1073,45 +1229,45 @@ class AddSearchColumn < ActiveRecord::Migration[5.1]
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
## GraphQL with React
|
## API Security
|
||||||
|
|
||||||
This is a quick simple example using `graphql.js` [https://github.com/f/graphql.js/](https://github.com/f/graphql.js/)
|
One of the the most common questions I get asked is what happens if a user out on the internet sends queries
|
||||||
|
that we don't want run. For example how do we stop him from fetching all users or the emails of users. Our answer to this is that it is not an issue as this cannot happen, let me explain.
|
||||||
|
|
||||||
```js
|
Super Graph runs in one of two modes `development` or `production`, this is controlled via the config value `production: false` when it's false it's running in development mode and when true, production. In development mode all the **named** queries (including mutations) are saved to the allow list `./config/allow.list`. While in production mode when Super Graph starts only the queries from this allow list file are registered with the database as [prepared statements](https://stackoverflow.com/questions/8263371/how-can-prepared-statements-protect-from-sql-injection-attacks).
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import graphql from 'graphql.js'
|
|
||||||
|
|
||||||
// Create a GraphQL client pointing to Super Graph
|
Prepared statements are designed by databases to be fast and secure. They protect against all kinds of sql injection attacks and since they are pre-processed and pre-planned they are much faster to run then raw sql queries. Also there's no GraphQL to SQL compiling happening in production mode which makes your queries lighting fast as they are directly sent to the database with almost no overhead.
|
||||||
var graph = graphql("http://localhost:3000/api/v1/graphql", { asJSON: true })
|
|
||||||
|
|
||||||
const App = () => {
|
In short in production only queries listed in the allow list file `./config/allow.list` can be used, all other queries will be blocked.
|
||||||
const [user, setUser] = useState(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
::: tip How to think about the allow list?
|
||||||
async function action() {
|
The allow list file is essentially a list of all your exposed API calls and the data that passes within them. It's very easy to build tooling to do things like parsing this file within your tests to ensure fields like `credit_card_no` are not accidently leaked. It's a great way to build compliance tooling and ensure your user data is always safe.
|
||||||
// Use the GraphQL client to execute a graphQL query
|
:::
|
||||||
// The second argument to the client are the variables you need to pass
|
|
||||||
const result = await graph(`{ user { id first_name last_name picture_url } }`)()
|
This is an example of a named query, `getUserWithProducts` is the name you've given to this query it can be anything you like but should be unique across all you're queries. Only named queries are saved in the allow list in development mode.
|
||||||
setUser(result)
|
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query getUserWithProducts {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
products {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
price
|
||||||
|
}
|
||||||
}
|
}
|
||||||
action()
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<h1>{ JSON.stringify(user) }</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
You can only have one type of auth enabled. You can either pick Rails or JWT.
|
You can only have one type of auth enabled either Rails or JWT.
|
||||||
|
|
||||||
### Rails Auth (Devise / Warden)
|
### Ruby on Rails
|
||||||
|
|
||||||
Almost all Rails apps use Devise or Warden for authentication. Once the user is
|
Almost all Rails apps use Devise or Warden for authentication. Once the user is
|
||||||
authenticated a session is created with the users ID. The session can either be
|
authenticated a session is created with the users ID. The session can either be
|
||||||
@ -1163,7 +1319,7 @@ auth:
|
|||||||
max_active: 12000
|
max_active: 12000
|
||||||
```
|
```
|
||||||
|
|
||||||
### JWT Token Auth
|
### JWT Tokens
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
auth:
|
auth:
|
||||||
@ -1177,14 +1333,67 @@ auth:
|
|||||||
public_key_type: ecdsa #rsa
|
public_key_type: ecdsa #rsa
|
||||||
```
|
```
|
||||||
|
|
||||||
For JWT tokens we currently support tokens from a provider like Auth0
|
For JWT tokens we currently support tokens from a provider like Auth0 or if you have a custom solution then we look for the `user_id` in the `subject` claim of of the `id token`. If you pick Auth0 then we derive two variables from the token `user_id` and `user_id_provider` for to use in your filters.
|
||||||
or if you have a custom solution then we look for the `user_id` in the
|
|
||||||
`subject` claim of of the `id token`. If you pick Auth0 then we derive two variables from the token `user_id` and `user_id_provider` for to use in your filters.
|
|
||||||
|
|
||||||
We can get the JWT token either from the `authorization` header where we expect it to be a `bearer` token or if `cookie` is specified then we look there.
|
We can get the JWT token either from the `authorization` header where we expect it to be a `bearer` token or if `cookie` is specified then we look there.
|
||||||
|
|
||||||
For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
|
For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
|
||||||
|
|
||||||
|
### HTTP Headers
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
header:
|
||||||
|
name: X-AppEngine-QueueName
|
||||||
|
exists: true
|
||||||
|
#value: default
|
||||||
|
```
|
||||||
|
|
||||||
|
Header auth is usually the best option to authenticate requests to the action endpoints. For example you
|
||||||
|
might want to use an action to refresh a materalized view every hour and only want a cron service like the Google AppEngine Cron service to make that request in this case a config similar to the one above will do.
|
||||||
|
|
||||||
|
The `exists: true` parameter ensures that only the existance of the header is checked not its value. The `value` parameter lets you confirm that the value matches the one assgined to the parameter. This helps in the case you are using a shared secret to protect the endpoint.
|
||||||
|
|
||||||
|
### Named Auth
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# You can add additional named auths to use with actions
|
||||||
|
# In this example actions using this auth can only be
|
||||||
|
# called from the Google Appengine Cron service that
|
||||||
|
# sets a special header to all it's requests
|
||||||
|
auths:
|
||||||
|
- name: from_taskqueue
|
||||||
|
type: header
|
||||||
|
header:
|
||||||
|
name: X-Appengine-Cron
|
||||||
|
exists: true
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition to the default auth configuration you can create additional named auth configurations to be used
|
||||||
|
with features like `actions`. For example while your main GraphQL endpoint uses JWT for authentication you may want to use a header value to ensure your actions can only be called by clients having access to a shared secret
|
||||||
|
or security header.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
Actions is a very useful feature that is currently work in progress. For now the best use case for actions is to
|
||||||
|
refresh database tables like materialized views or call a database procedure to refresh a cache table, etc. An action creates an http endpoint that anyone can call to have the SQL query executed. The below example will create an endpoint `/api/v1/actions/refresh_leaderboard_users` any request send to that endpoint will cause the sql query to be executed. the `auth_name` points to a named auth that should be used to secure this endpoint. In future we have big plans to allow your own custom code to run using actions.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
actions:
|
||||||
|
- name: refresh_leaderboard_users
|
||||||
|
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||||
|
auth_name: from_taskqueue
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using CURL to test a query
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# fetch the response json directly from the endpoint using user id 5
|
||||||
|
curl 'http://localhost:8080/api/v1/graphql' \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-H 'X-User-ID: 5' \
|
||||||
|
--data-binary '{"query":"{ products { name price users { email }}}"}'
|
||||||
|
```
|
||||||
|
|
||||||
## Access Control
|
## Access Control
|
||||||
|
|
||||||
It's common for APIs to control what information they return or insert based on the role of the user. In Super Graph we have two primary roles `user` and `anon` the first for users where a `user_id` is available the latter for users where it's not.
|
It's common for APIs to control what information they return or insert based on the role of the user. In Super Graph we have two primary roles `user` and `anon` the first for users where a `user_id` is available the latter for users where it's not.
|
||||||
@ -1197,7 +1406,6 @@ The `user` role can be divided up into further roles based on attributes in the
|
|||||||
|
|
||||||
Super Graph allows you to create roles dynamically using a `roles_query` and ` match` config values.
|
Super Graph allows you to create roles dynamically using a `roles_query` and ` match` config values.
|
||||||
|
|
||||||
|
|
||||||
### Configure RBAC
|
### Configure RBAC
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@ -1430,6 +1638,22 @@ auth:
|
|||||||
# public_key_file: /secrets/public_key.pem
|
# public_key_file: /secrets/public_key.pem
|
||||||
# public_key_type: ecdsa #rsa
|
# public_key_type: ecdsa #rsa
|
||||||
|
|
||||||
|
# header:
|
||||||
|
# name: dnt
|
||||||
|
# exists: true
|
||||||
|
# value: localhost:8080
|
||||||
|
|
||||||
|
# You can add additional named auths to use with actions
|
||||||
|
# In this example actions using this auth can only be
|
||||||
|
# called from the Google Appengine Cron service that
|
||||||
|
# sets a special header to all it's requests
|
||||||
|
auths:
|
||||||
|
- name: from_taskqueue
|
||||||
|
type: header
|
||||||
|
header:
|
||||||
|
name: X-Appengine-Cron
|
||||||
|
exists: true
|
||||||
|
|
||||||
database:
|
database:
|
||||||
type: postgres
|
type: postgres
|
||||||
host: db
|
host: db
|
||||||
@ -1460,6 +1684,17 @@ database:
|
|||||||
- encrypted
|
- encrypted
|
||||||
- token
|
- token
|
||||||
|
|
||||||
|
# Create custom actions with their own api endpoints
|
||||||
|
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
|
||||||
|
# A request to this url will execute the configured SQL query
|
||||||
|
# which in this case refreshes a materialized view in the database.
|
||||||
|
# The auth_name is from one of the configured auths
|
||||||
|
actions:
|
||||||
|
- name: refresh_leaderboard_users
|
||||||
|
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||||
|
auth_name: from_taskqueue
|
||||||
|
|
||||||
|
|
||||||
tables:
|
tables:
|
||||||
- name: customers
|
- name: customers
|
||||||
remotes:
|
remotes:
|
||||||
|
1
go.mod
1
go.mod
@ -12,6 +12,7 @@ require (
|
|||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/dlclark/regexp2 v1.2.0 // indirect
|
github.com/dlclark/regexp2 v1.2.0 // indirect
|
||||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733
|
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733
|
||||||
|
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c // indirect
|
||||||
github.com/fsnotify/fsnotify v1.4.7
|
github.com/fsnotify/fsnotify v1.4.7
|
||||||
github.com/garyburd/redigo v1.6.0
|
github.com/garyburd/redigo v1.6.0
|
||||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -54,6 +54,8 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
|
|||||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw=
|
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw=
|
||||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
||||||
|
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c h1:/bXaeEuNG6V0HeyEGw11DYLW5BGsOPlcVRIXbHNUWSo=
|
||||||
|
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||||
|
9
jsn/bench.0
Normal file
9
jsn/bench.0
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
goos: darwin
|
||||||
|
goarch: amd64
|
||||||
|
pkg: github.com/dosco/super-graph/jsn
|
||||||
|
BenchmarkGet-8 13310 88437 ns/op 3328 B/op 2 allocs/op
|
||||||
|
BenchmarkFilter-8 182232 6922 ns/op 448 B/op 1 allocs/op
|
||||||
|
BenchmarkStrip-8 162709 6560 ns/op 224 B/op 1 allocs/op
|
||||||
|
BenchmarkReplace-8 85846 13996 ns/op 416 B/op 1 allocs/op
|
||||||
|
PASS
|
||||||
|
ok github.com/dosco/super-graph/jsn 5.913s
|
@ -64,7 +64,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && b[i] == '"':
|
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
k = b[(s + 1):i]
|
k = b[(s + 1):i]
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
|||||||
case state == expectValue && b[i] == '"':
|
case state == expectValue && b[i] == '"':
|
||||||
state = expectString
|
state = expectString
|
||||||
|
|
||||||
case state == expectString && b[i] == '"':
|
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '[':
|
case state == expectValue && b[i] == '[':
|
||||||
|
18
jsn/get.go
18
jsn/get.go
@ -66,7 +66,7 @@ func Get(b []byte, keys [][]byte) []Field {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && b[i] == '"':
|
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
k = b[(s + 1):i]
|
k = b[(s + 1):i]
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ func Get(b []byte, keys [][]byte) []Field {
|
|||||||
state = expectString
|
state = expectString
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectString && b[i] == '"':
|
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '[':
|
case state == expectValue && b[i] == '[':
|
||||||
@ -130,6 +130,20 @@ func Get(b []byte, keys [][]byte) []Field {
|
|||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state == expectListClose {
|
||||||
|
loop:
|
||||||
|
for j := i + 1; j < len(b); j++ {
|
||||||
|
switch b[j] {
|
||||||
|
case ' ', '\t', '\n':
|
||||||
|
continue
|
||||||
|
case '{':
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
i = e
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state = expectKey
|
state = expectKey
|
||||||
e = 0
|
e = 0
|
||||||
}
|
}
|
||||||
|
@ -9,16 +9,16 @@ var (
|
|||||||
input1 = `
|
input1 = `
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"test": { "__twitter_id": "ABCD" },
|
"test_1a": { "__twitter_id": "ABCD" },
|
||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"full_name": "Sidney Stroman",
|
"full_name": "'Sidney St[1]roman'",
|
||||||
"email": "user0@demo.com",
|
"email": "user0@demo.com",
|
||||||
"__twitter_id": "2048666903444506956",
|
"__twitter_id": "2048666903444506956",
|
||||||
"embed": {
|
"embed": {
|
||||||
"id": 8,
|
"id": 8,
|
||||||
"full_name": "Caroll Orn Sr.",
|
"full_name": "Caroll Orn Sr's",
|
||||||
"email": "joannarau@hegmann.io",
|
"email": "joannarau@hegmann.io",
|
||||||
"__twitter_id": "ABC123"
|
"__twitter_id": "ABC123"
|
||||||
"more": [{
|
"more": [{
|
||||||
@ -37,7 +37,7 @@ var (
|
|||||||
"id": 3,
|
"id": 3,
|
||||||
"full_name": "Kenna Cassin",
|
"full_name": "Kenna Cassin",
|
||||||
"email": "user2@demo.com",
|
"email": "user2@demo.com",
|
||||||
"__twitter_id": { "name": "hello", "address": { "work": "1 infinity loop" } }
|
"__twitter_id": { "name": "\"hellos\"", "address": { "work": "1 infinity loop" } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
@ -108,7 +108,7 @@ var (
|
|||||||
input2 = `
|
input2 = `
|
||||||
[{
|
[{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"full_name": "Sidney Stroman",
|
"full_name": "Sidney St[1]roman",
|
||||||
"email": "user0@demo.com",
|
"email": "user0@demo.com",
|
||||||
"__twitter_id": "2048666903444506956",
|
"__twitter_id": "2048666903444506956",
|
||||||
"something": null,
|
"something": null,
|
||||||
@ -130,7 +130,7 @@ var (
|
|||||||
input3 = `
|
input3 = `
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"test": { "__twitter_id": "ABCD" },
|
"test_1a": { "__twitter_id": "ABCD" },
|
||||||
"users": [{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]
|
"users": [{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
@ -138,7 +138,7 @@ var (
|
|||||||
input4 = `
|
input4 = `
|
||||||
{ "users" : [{
|
{ "users" : [{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"full_name": "Sidney Stroman",
|
"full_name": "Sidney St[1]roman",
|
||||||
"email": "user0@demo.com",
|
"email": "user0@demo.com",
|
||||||
"__twitter_id": "2048666903444506956",
|
"__twitter_id": "2048666903444506956",
|
||||||
"embed": {
|
"embed": {
|
||||||
@ -155,24 +155,26 @@ var (
|
|||||||
"email": "user1@demo.com",
|
"email": "user1@demo.com",
|
||||||
"__twitter_id": [{ "name": "hello" }, { "name": "world"}]
|
"__twitter_id": [{ "name": "hello" }, { "name": "world"}]
|
||||||
}] }`
|
}] }`
|
||||||
|
|
||||||
|
input5 = `
|
||||||
|
{"data":{"title":"In September 2018, Slovak police stated that Kuciak was murdered because of his investigative work, and that the murder had been ordered.[9][10] They arrested eight suspects,[11] charging three of them with first-degree murder.[11]","topics":["cpp"]},"a":["1111"]},"thread_slug":"in-september-2018-slovak-police-stated-that-kuciak-7929",}`
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
func TestGet(t *testing.T) {
|
||||||
values := Get([]byte(input1), [][]byte{
|
values := Get([]byte(input1), [][]byte{
|
||||||
|
[]byte("test_1a"),
|
||||||
[]byte("__twitter_id"),
|
[]byte("__twitter_id"),
|
||||||
[]byte("work_email"),
|
[]byte("work_email"),
|
||||||
})
|
})
|
||||||
|
|
||||||
expected := []Field{
|
expected := []Field{
|
||||||
|
{[]byte("test_1a"), []byte(`{ "__twitter_id": "ABCD" }`)},
|
||||||
{[]byte("__twitter_id"), []byte(`"ABCD"`)},
|
{[]byte("__twitter_id"), []byte(`"ABCD"`)},
|
||||||
{[]byte("__twitter_id"), []byte(`"2048666903444506956"`)},
|
{[]byte("__twitter_id"), []byte(`"2048666903444506956"`)},
|
||||||
{[]byte("__twitter_id"), []byte(`"ABC123"`)},
|
{[]byte("__twitter_id"), []byte(`"ABC123"`)},
|
||||||
{[]byte("__twitter_id"), []byte(`"more123"`)},
|
{[]byte("__twitter_id"), []byte(`"more123"`)},
|
||||||
{[]byte("__twitter_id"),
|
{[]byte("__twitter_id"), []byte(`[{ "name": "hello" }, { "name": "world"}]`)},
|
||||||
[]byte(`[{ "name": "hello" }, { "name": "world"}]`)},
|
{[]byte("__twitter_id"), []byte(`{ "name": "\"hellos\"", "address": { "work": "1 infinity loop" } }`)},
|
||||||
{[]byte("__twitter_id"),
|
|
||||||
[]byte(`{ "name": "hello", "address": { "work": "1 infinity loop" } }`),
|
|
||||||
},
|
|
||||||
{[]byte("__twitter_id"), []byte(`1234567890`)},
|
{[]byte("__twitter_id"), []byte(`1234567890`)},
|
||||||
{[]byte("__twitter_id"), []byte(`1.23E`)},
|
{[]byte("__twitter_id"), []byte(`1.23E`)},
|
||||||
{[]byte("__twitter_id"), []byte(`true`)},
|
{[]byte("__twitter_id"), []byte(`true`)},
|
||||||
@ -201,6 +203,30 @@ func TestGet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGet1(t *testing.T) {
|
||||||
|
values := Get([]byte(input5), [][]byte{
|
||||||
|
[]byte("thread_slug"),
|
||||||
|
})
|
||||||
|
|
||||||
|
expected := []Field{
|
||||||
|
{[]byte("thread_slug"), []byte(`"in-september-2018-slovak-police-stated-that-kuciak-7929"`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(values) != len(expected) {
|
||||||
|
t.Fatal("len(values) != len(expected)")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range expected {
|
||||||
|
if !bytes.Equal(values[i].Key, expected[i].Key) {
|
||||||
|
t.Error(string(values[i].Key), " != ", string(expected[i].Key))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(values[i].Value, expected[i].Value) {
|
||||||
|
t.Error(string(values[i].Value), " != ", string(expected[i].Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValue(t *testing.T) {
|
func TestValue(t *testing.T) {
|
||||||
v1 := []byte("12345")
|
v1 := []byte("12345")
|
||||||
if !bytes.Equal(Value(v1), v1) {
|
if !bytes.Equal(Value(v1), v1) {
|
||||||
@ -230,7 +256,7 @@ func TestFilter1(t *testing.T) {
|
|||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := `[{"id": 1,"full_name": "Sidney Stroman","embed": {"id": 8,"full_name": "Caroll Orn Sr.","email": "joannarau@hegmann.io","__twitter_id": "ABC123"}},{"id": 2,"full_name": "Jerry Dickinson"}]`
|
expected := `[{"id": 1,"full_name": "Sidney St[1]roman","embed": {"id": 8,"full_name": "Caroll Orn Sr.","email": "joannarau@hegmann.io","__twitter_id": "ABC123"}},{"id": 2,"full_name": "Jerry Dickinson"}]`
|
||||||
|
|
||||||
if b.String() != expected {
|
if b.String() != expected {
|
||||||
t.Error("Does not match expected json")
|
t.Error("Does not match expected json")
|
||||||
@ -306,7 +332,7 @@ func TestReplace(t *testing.T) {
|
|||||||
|
|
||||||
expected := `{ "users" : [{
|
expected := `{ "users" : [{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"full_name": "Sidney Stroman",
|
"full_name": "Sidney St[1]roman",
|
||||||
"email": "user0@demo.com",
|
"email": "user0@demo.com",
|
||||||
"__twitter_id": "2048666903444506956",
|
"__twitter_id": "2048666903444506956",
|
||||||
"embed": {
|
"embed": {
|
||||||
@ -338,7 +364,7 @@ func TestReplace(t *testing.T) {
|
|||||||
func TestReplaceEmpty(t *testing.T) {
|
func TestReplaceEmpty(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
json := `{ "users" : [{"id":1,"full_name":"Sidney Stroman","email":"user0@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":2,"full_name":"Jerry Dickinson","email":"user1@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":3,"full_name":"Kenna Cassin","email":"user2@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":4,"full_name":"Mr. Pat Parisian","email":"rodney@kautzer.biz","__users_twitter_id":"2048666903444506956"}, {"id":5,"full_name":"Bette Ebert","email":"janeenrath@goyette.com","__users_twitter_id":"2048666903444506956"}, {"id":6,"full_name":"Everett Kiehn","email":"michael@bartoletti.com","__users_twitter_id":"2048666903444506956"}, {"id":7,"full_name":"Katrina Cronin","email":"loretaklocko@framivolkman.org","__users_twitter_id":"2048666903444506956"}, {"id":8,"full_name":"Caroll Orn Sr.","email":"joannarau@hegmann.io","__users_twitter_id":"2048666903444506956"}, {"id":9,"full_name":"Gwendolyn Ziemann","email":"renaytoy@rutherford.co","__users_twitter_id":"2048666903444506956"}, {"id":10,"full_name":"Mrs. Rosann Fritsch","email":"holliemosciski@thiel.org","__users_twitter_id":"2048666903444506956"}, {"id":11,"full_name":"Arden Koss","email":"cristobalankunding@howewelch.org","__users_twitter_id":"2048666903444506956"}, {"id":12,"full_name":"Brenton Bauch PhD","email":"renee@miller.co","__users_twitter_id":"2048666903444506956"}, {"id":13,"full_name":"Daine Gleichner","email":"andrea@nienow.co","__users_twitter_id":"2048666903444506956"}] }`
|
json := `{ "users" : [{"id":1,"full_name":"Sidney St[1]roman","email":"user0@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":2,"full_name":"Jerry Dickinson","email":"user1@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":3,"full_name":"Kenna Cassin","email":"user2@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":4,"full_name":"Mr. Pat Parisian","email":"rodney@kautzer.biz","__users_twitter_id":"2048666903444506956"}, {"id":5,"full_name":"Bette Ebert","email":"janeenrath@goyette.com","__users_twitter_id":"2048666903444506956"}, {"id":6,"full_name":"Everett Kiehn","email":"michael@bartoletti.com","__users_twitter_id":"2048666903444506956"}, {"id":7,"full_name":"Katrina Cronin","email":"loretaklocko@framivolkman.org","__users_twitter_id":"2048666903444506956"}, {"id":8,"full_name":"Caroll Orn Sr.","email":"joannarau@hegmann.io","__users_twitter_id":"2048666903444506956"}, {"id":9,"full_name":"Gwendolyn Ziemann","email":"renaytoy@rutherford.co","__users_twitter_id":"2048666903444506956"}, {"id":10,"full_name":"Mrs. Rosann Fritsch","email":"holliemosciski@thiel.org","__users_twitter_id":"2048666903444506956"}, {"id":11,"full_name":"Arden Koss","email":"cristobalankunding@howewelch.org","__users_twitter_id":"2048666903444506956"}, {"id":12,"full_name":"Brenton Bauch PhD","email":"renee@miller.co","__users_twitter_id":"2048666903444506956"}, {"id":13,"full_name":"Daine Gleichner","email":"andrea@nienow.co","__users_twitter_id":"2048666903444506956"}] }`
|
||||||
|
|
||||||
err := Replace(&buf, []byte(json), []Field{}, []Field{})
|
err := Replace(&buf, []byte(json), []Field{}, []Field{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -395,7 +421,7 @@ func TestKeys3(t *testing.T) {
|
|||||||
json := `{
|
json := `{
|
||||||
"insert": {
|
"insert": {
|
||||||
"created_at": "now",
|
"created_at": "now",
|
||||||
"test": { "type1": "a", "type2": "b" },
|
"test_1a": { "type1": "a", "type2": "b" },
|
||||||
"name": "Hello",
|
"name": "Hello",
|
||||||
"updated_at": "now",
|
"updated_at": "now",
|
||||||
"description": "World"
|
"description": "World"
|
||||||
@ -406,7 +432,7 @@ func TestKeys3(t *testing.T) {
|
|||||||
fields := Keys([]byte(json))
|
fields := Keys([]byte(json))
|
||||||
|
|
||||||
exp := []string{
|
exp := []string{
|
||||||
"insert", "created_at", "test", "type1", "type2", "name", "updated_at", "description",
|
"insert", "created_at", "test_1a", "type1", "type2", "name", "updated_at", "description",
|
||||||
"user",
|
"user",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
jsn/keys.go
17
jsn/keys.go
@ -47,7 +47,7 @@ func Keys(b []byte) [][]byte {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && b[i] == '"':
|
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
k = b[(s + 1):i]
|
k = b[(s + 1):i]
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ func Keys(b []byte) [][]byte {
|
|||||||
state = expectString
|
state = expectString
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectString && b[i] == '"':
|
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '{':
|
case state == expectValue && b[i] == '{':
|
||||||
@ -111,6 +111,19 @@ func Keys(b []byte) [][]byte {
|
|||||||
res = append(res, k)
|
res = append(res, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state == expectListClose {
|
||||||
|
loop:
|
||||||
|
for j := i + 1; j < len(b); j++ {
|
||||||
|
switch b[j] {
|
||||||
|
case ' ', '\t', '\n':
|
||||||
|
continue
|
||||||
|
case '{':
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
i = e
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
state = expectKey
|
state = expectKey
|
||||||
k = nil
|
k = nil
|
||||||
e = 0
|
e = 0
|
||||||
|
@ -52,7 +52,7 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && b[i] == '"':
|
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
if _, err := h.Write(b[(s + 1):i]); err != nil {
|
if _, err := h.Write(b[(s + 1):i]); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -66,7 +66,7 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
|||||||
state = expectString
|
state = expectString
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectString && b[i] == '"':
|
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '[':
|
case state == expectValue && b[i] == '[':
|
||||||
|
@ -27,7 +27,7 @@ func Strip(b []byte, path [][]byte) []byte {
|
|||||||
state = expectKeyClose
|
state = expectKeyClose
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectKeyClose && b[i] == '"':
|
case state == expectKeyClose && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
state = expectColon
|
state = expectColon
|
||||||
if pi == len(path) {
|
if pi == len(path) {
|
||||||
pi = 0
|
pi = 0
|
||||||
@ -44,7 +44,7 @@ func Strip(b []byte, path [][]byte) []byte {
|
|||||||
state = expectString
|
state = expectString
|
||||||
s = i
|
s = i
|
||||||
|
|
||||||
case state == expectString && b[i] == '"':
|
case state == expectString && (b[i-1] != '\\' && b[i] == '"'):
|
||||||
e = i
|
e = i
|
||||||
|
|
||||||
case state == expectValue && b[i] == '[':
|
case state == expectValue && b[i] == '[':
|
||||||
|
2
main.go
2
main.go
@ -5,5 +5,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
serv.Init()
|
serv.Cmd()
|
||||||
}
|
}
|
||||||
|
54
psql/fuzz.go
Normal file
54
psql/fuzz.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package psql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/dosco/super-graph/qcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
qcompileTest, _ = qcode.NewCompiler(qcode.Config{})
|
||||||
|
|
||||||
|
schema = getTestSchema()
|
||||||
|
|
||||||
|
vars = NewVariables(map[string]string{
|
||||||
|
"admin_account_id": "5",
|
||||||
|
})
|
||||||
|
|
||||||
|
pcompileTest = NewCompiler(Config{
|
||||||
|
Schema: schema,
|
||||||
|
Vars: vars,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuzzerEntrypoint for Fuzzbuzz
|
||||||
|
func Fuzz(data []byte) int {
|
||||||
|
gql := `mutation {
|
||||||
|
product(insert: $data) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
full_name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
qc, err := qcompileTest.Compile([]byte(gql), "user")
|
||||||
|
if err != nil {
|
||||||
|
panic("qcompile can't fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := map[string]json.RawMessage{
|
||||||
|
"data": json.RawMessage(data),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = pcompileTest.CompileEx(qc, vars)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
@ -15,7 +15,10 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer,
|
|||||||
|
|
||||||
insert, ok := vars[qc.ActionVar]
|
insert, ok := vars[qc.ActionVar]
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, fmt.Errorf("Variable '%s' not !defined", qc.ActionVar)
|
return 0, fmt.Errorf("variable '%s' not defined", qc.ActionVar)
|
||||||
|
}
|
||||||
|
if len(insert) == 0 {
|
||||||
|
return 0, fmt.Errorf("variable '%s' is empty", qc.ActionVar)
|
||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
|
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
|
||||||
@ -147,7 +150,14 @@ func renderNestedInsertRelColumns(w io.Writer, item kvitem, values bool) error {
|
|||||||
io.WriteString(w, `, `)
|
io.WriteString(w, `, `)
|
||||||
}
|
}
|
||||||
if values {
|
if values {
|
||||||
|
if v._ctype > 0 {
|
||||||
|
io.WriteString(w, `"_x_`)
|
||||||
|
io.WriteString(w, v.relCP.Left.Table)
|
||||||
|
io.WriteString(w, `".`)
|
||||||
|
quoted(w, v.relCP.Left.Col)
|
||||||
|
} else {
|
||||||
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col)
|
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
quoted(w, v.relCP.Right.Col)
|
quoted(w, v.relCP.Right.Col)
|
||||||
}
|
}
|
||||||
@ -166,15 +176,21 @@ func renderNestedInsertRelTables(w io.Writer, item kvitem) error {
|
|||||||
io.WriteString(w, `, `)
|
io.WriteString(w, `, `)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Render child foreign key columns if child-to-parent
|
// Render tables needed to set values if child-to-parent
|
||||||
// relationship is one-to-many
|
// relationship is one-to-many
|
||||||
for _, v := range item.items {
|
for _, v := range item.items {
|
||||||
if v.relCP.Type == RelOneToMany {
|
if v.relCP.Type == RelOneToMany {
|
||||||
|
if v._ctype > 0 {
|
||||||
|
io.WriteString(w, `"_x_`)
|
||||||
|
io.WriteString(w, v.relCP.Left.Table)
|
||||||
|
io.WriteString(w, `", `)
|
||||||
|
} else {
|
||||||
quoted(w, v.relCP.Left.Table)
|
quoted(w, v.relCP.Left.Table)
|
||||||
io.WriteString(w, `, `)
|
io.WriteString(w, `, `)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -249,7 +249,7 @@ func nestedInsertOneToManyWithConnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id"FROM "users" WHERE ("products"."id" = '5') RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
@ -278,6 +278,10 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
|
|||||||
product(insert: $data) {
|
product(insert: $data) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
full_name
|
full_name
|
||||||
@ -286,7 +290,7 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."id" = '5' LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user", "tags_2_join"."json_2" AS "tags") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_2"), '[]') AS "json_2" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "tags_2"."id" AS "id", "tags_2"."name" AS "name") AS "json_row_2")) AS "json_2" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2" LIMIT ('20') :: integer) AS "json_agg_2") AS "tags_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
@ -310,6 +314,43 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nestedInsertOneToOneWithConnectArray(t *testing.T) {
|
||||||
|
gql := `mutation {
|
||||||
|
product(insert: $data) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
full_name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id" = ANY((select a::bigint AS list from json_array_elements_text((i.j->'user'->'connect'->>'id')::json) AS a)) LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
|
vars := map[string]json.RawMessage{
|
||||||
|
"data": json.RawMessage(`{
|
||||||
|
"name": "Apple",
|
||||||
|
"price": 1.25,
|
||||||
|
"created_at": "now",
|
||||||
|
"updated_at": "now",
|
||||||
|
"user": {
|
||||||
|
"connect": { "id": [1,2] }
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(resSQL) != sql {
|
||||||
|
t.Fatal(errNotExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCompileInsert(t *testing.T) {
|
func TestCompileInsert(t *testing.T) {
|
||||||
t.Run("simpleInsert", simpleInsert)
|
t.Run("simpleInsert", simpleInsert)
|
||||||
t.Run("singleInsert", singleInsert)
|
t.Run("singleInsert", singleInsert)
|
||||||
@ -320,4 +361,6 @@ func TestCompileInsert(t *testing.T) {
|
|||||||
t.Run("nestedInsertOneToOne", nestedInsertOneToOne)
|
t.Run("nestedInsertOneToOne", nestedInsertOneToOne)
|
||||||
t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect)
|
t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect)
|
||||||
t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect)
|
t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect)
|
||||||
|
t.Run("nestedInsertOneToOneWithConnectArray", nestedInsertOneToOneWithConnectArray)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
158
psql/mutate.go
158
psql/mutate.go
@ -101,6 +101,9 @@ type renitem struct {
|
|||||||
data map[string]json.RawMessage
|
data map[string]json.RawMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Handle cases where a column name matches the child table name
|
||||||
|
// the child path needs to be exluded in the json sent to insert or update
|
||||||
|
|
||||||
func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
|
func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
|
||||||
var data map[string]json.RawMessage
|
var data map[string]json.RawMessage
|
||||||
var array bool
|
var array bool
|
||||||
@ -124,9 +127,6 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
|
|||||||
if v[0] != '{' && v[0] != '[' {
|
if v[0] != '{' && v[0] != '[' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := item.ti.ColMap[k]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get child-to-parent relationship
|
// Get child-to-parent relationship
|
||||||
relCP, err := c.schema.GetRel(k, item.key)
|
relCP, err := c.schema.GetRel(k, item.key)
|
||||||
@ -152,13 +152,9 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
|
|||||||
id++
|
id++
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
ti, err := c.schema.GetTable(k)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Get parent-to-child relationship
|
// Get parent-to-child relationship
|
||||||
relPC, err := c.schema.GetRel(item.key, k)
|
} else if relPC, err := c.schema.GetRel(item.key, k); err == nil {
|
||||||
|
ti, err := c.schema.GetTable(k)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -277,8 +273,12 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
|||||||
io.WriteString(w, ` SET `)
|
io.WriteString(w, ` SET `)
|
||||||
quoted(w, item.relPC.Right.Col)
|
quoted(w, item.relPC.Right.Col)
|
||||||
io.WriteString(w, ` = `)
|
io.WriteString(w, ` = `)
|
||||||
|
|
||||||
|
// When setting the id of the connected table in a one-to-many setting
|
||||||
|
// we always overwrite the value including for array columns
|
||||||
colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col)
|
colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col)
|
||||||
io.WriteString(w, `FROM `)
|
|
||||||
|
io.WriteString(w, ` FROM `)
|
||||||
quoted(w, item.relPC.Left.Table)
|
quoted(w, item.relPC.Left.Table)
|
||||||
io.WriteString(w, ` WHERE`)
|
io.WriteString(w, ` WHERE`)
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
|||||||
} else {
|
} else {
|
||||||
io.WriteString(w, ` (`)
|
io.WriteString(w, ` (`)
|
||||||
}
|
}
|
||||||
if err := renderKVItemWhere(w, v); err != nil {
|
if err := renderWhereFromJSON(w, v, "connect", v.val); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
io.WriteString(w, `)`)
|
io.WriteString(w, `)`)
|
||||||
@ -313,7 +313,19 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
|||||||
quoted(w, item.ti.Name)
|
quoted(w, item.ti.Name)
|
||||||
io.WriteString(w, ` SET `)
|
io.WriteString(w, ` SET `)
|
||||||
quoted(w, item.relPC.Right.Col)
|
quoted(w, item.relPC.Right.Col)
|
||||||
io.WriteString(w, ` = NULL`)
|
io.WriteString(w, ` = `)
|
||||||
|
|
||||||
|
if item.relPC.Right.Array {
|
||||||
|
io.WriteString(w, ` array_remove(`)
|
||||||
|
quoted(w, item.relPC.Right.Col)
|
||||||
|
io.WriteString(w, `, `)
|
||||||
|
colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col)
|
||||||
|
io.WriteString(w, `)`)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
io.WriteString(w, ` NULL`)
|
||||||
|
}
|
||||||
|
|
||||||
io.WriteString(w, ` FROM `)
|
io.WriteString(w, ` FROM `)
|
||||||
quoted(w, item.relPC.Left.Table)
|
quoted(w, item.relPC.Left.Table)
|
||||||
io.WriteString(w, ` WHERE`)
|
io.WriteString(w, ` WHERE`)
|
||||||
@ -326,7 +338,7 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
|||||||
} else {
|
} else {
|
||||||
io.WriteString(w, ` (`)
|
io.WriteString(w, ` (`)
|
||||||
}
|
}
|
||||||
if err := renderKVItemWhere(w, v); err != nil {
|
if err := renderWhereFromJSON(w, v, "disconnect", v.val); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
io.WriteString(w, `)`)
|
io.WriteString(w, `)`)
|
||||||
@ -335,10 +347,11 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
|||||||
}
|
}
|
||||||
io.WriteString(w, ` RETURNING `)
|
io.WriteString(w, ` RETURNING `)
|
||||||
quoted(w, item.ti.Name)
|
quoted(w, item.ti.Name)
|
||||||
io.WriteString(w, `.*), `)
|
io.WriteString(w, `.*)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if connect && disconnect {
|
if connect && disconnect {
|
||||||
|
io.WriteString(w, `, `)
|
||||||
quoted(w, item.ti.Name)
|
quoted(w, item.ti.Name)
|
||||||
io.WriteString(w, ` AS (`)
|
io.WriteString(w, ` AS (`)
|
||||||
io.WriteString(w, `SELECT * FROM `)
|
io.WriteString(w, `SELECT * FROM `)
|
||||||
@ -433,7 +446,10 @@ func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer,
|
|||||||
|
|
||||||
upsert, ok := vars[qc.ActionVar]
|
upsert, ok := vars[qc.ActionVar]
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, fmt.Errorf("Variable '%s' not defined", qc.ActionVar)
|
return 0, fmt.Errorf("variable '%s' not defined", qc.ActionVar)
|
||||||
|
}
|
||||||
|
if len(upsert) == 0 {
|
||||||
|
return 0, fmt.Errorf("variable '%s' is empty", qc.ActionVar)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ti.PrimaryCol == nil {
|
if ti.PrimaryCol == nil {
|
||||||
@ -507,18 +523,33 @@ func (c *compilerContext) renderConnectStmt(qc *qcode.QCode, w io.Writer,
|
|||||||
rel := item.relPC
|
rel := item.relPC
|
||||||
|
|
||||||
// Render only for parent-to-child relationship of one-to-one
|
// Render only for parent-to-child relationship of one-to-one
|
||||||
|
// For this to work the child needs to found first so it's primary key
|
||||||
|
// can be set in the related column on the parent object.
|
||||||
|
// Eg. Create product and connect a user to it.
|
||||||
if rel.Type != RelOneToOne {
|
if rel.Type != RelOneToOne {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(w, `, `)
|
io.WriteString(w, `, "_x_`)
|
||||||
quoted(w, item.ti.Name)
|
io.WriteString(c.w, item.ti.Name)
|
||||||
io.WriteString(c.w, ` AS (`)
|
io.WriteString(c.w, `" AS (SELECT `)
|
||||||
|
|
||||||
io.WriteString(c.w, `SELECT * FROM `)
|
if rel.Left.Array {
|
||||||
|
io.WriteString(w, `array_agg(DISTINCT `)
|
||||||
|
quoted(w, rel.Right.Col)
|
||||||
|
io.WriteString(w, `) AS `)
|
||||||
|
quoted(w, rel.Right.Col)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
quoted(w, rel.Right.Col)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(c.w, ` FROM "_sg_input" i,`)
|
||||||
quoted(c.w, item.ti.Name)
|
quoted(c.w, item.ti.Name)
|
||||||
|
|
||||||
io.WriteString(c.w, ` WHERE `)
|
io.WriteString(c.w, ` WHERE `)
|
||||||
if err := renderKVItemWhere(c.w, item.kvitem); err != nil {
|
if err := renderWhereFromJSON(c.w, item.kvitem, "connect", item.kvitem.val); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
io.WriteString(c.w, ` LIMIT 1)`)
|
io.WriteString(c.w, ` LIMIT 1)`)
|
||||||
@ -532,50 +563,105 @@ func (c *compilerContext) renderDisconnectStmt(qc *qcode.QCode, w io.Writer,
|
|||||||
rel := item.relPC
|
rel := item.relPC
|
||||||
|
|
||||||
// Render only for parent-to-child relationship of one-to-one
|
// Render only for parent-to-child relationship of one-to-one
|
||||||
|
// For this to work the child needs to found first so it's
|
||||||
|
// null value can beset in the related column on the parent object.
|
||||||
|
// Eg. Update product and diconnect the user from it.
|
||||||
if rel.Type != RelOneToOne {
|
if rel.Type != RelOneToOne {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
io.WriteString(w, `, `)
|
io.WriteString(w, `, "_x_`)
|
||||||
quoted(w, item.ti.Name)
|
io.WriteString(c.w, item.ti.Name)
|
||||||
io.WriteString(c.w, ` AS (`)
|
io.WriteString(c.w, `" AS (`)
|
||||||
|
|
||||||
|
if rel.Right.Array {
|
||||||
|
io.WriteString(c.w, `SELECT `)
|
||||||
|
quoted(w, rel.Right.Col)
|
||||||
|
io.WriteString(c.w, ` FROM "_sg_input" i,`)
|
||||||
|
quoted(c.w, item.ti.Name)
|
||||||
|
io.WriteString(c.w, ` WHERE `)
|
||||||
|
if err := renderWhereFromJSON(c.w, item.kvitem, "connect", item.kvitem.val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
io.WriteString(c.w, ` LIMIT 1))`)
|
||||||
|
|
||||||
|
} else {
|
||||||
io.WriteString(c.w, `SELECT * FROM (VALUES(NULL::`)
|
io.WriteString(c.w, `SELECT * FROM (VALUES(NULL::`)
|
||||||
io.WriteString(w, rel.Right.col.Type)
|
io.WriteString(w, rel.Right.col.Type)
|
||||||
io.WriteString(c.w, `)) AS LOOKUP(`)
|
io.WriteString(c.w, `)) AS LOOKUP(`)
|
||||||
quoted(w, rel.Right.Col)
|
quoted(w, rel.Right.Col)
|
||||||
io.WriteString(c.w, `))`)
|
io.WriteString(c.w, `))`)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderKVItemWhere(w io.Writer, item kvitem) error {
|
func renderWhereFromJSON(w io.Writer, item kvitem, key string, val []byte) error {
|
||||||
return renderWhereFromJSON(w, item.ti.Name, item.val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderWhereFromJSON(w io.Writer, table string, val []byte) error {
|
|
||||||
var kv map[string]json.RawMessage
|
var kv map[string]json.RawMessage
|
||||||
|
ti := item.ti
|
||||||
|
|
||||||
if err := json.Unmarshal(val, &kv); err != nil {
|
if err := json.Unmarshal(val, &kv); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
i := 0
|
i := 0
|
||||||
for k, v := range kv {
|
for k, v := range kv {
|
||||||
|
col, ok := ti.ColMap[k]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
io.WriteString(w, ` AND `)
|
io.WriteString(w, ` AND `)
|
||||||
}
|
}
|
||||||
colWithTable(w, table, k)
|
|
||||||
io.WriteString(w, ` = '`)
|
if v[0] == '[' {
|
||||||
switch v[0] {
|
colWithTable(w, ti.Name, k)
|
||||||
case '"':
|
|
||||||
w.Write(v[1 : len(v)-1])
|
if col.Array {
|
||||||
default:
|
io.WriteString(w, ` && `)
|
||||||
w.Write(v)
|
} else {
|
||||||
|
io.WriteString(w, ` = `)
|
||||||
}
|
}
|
||||||
io.WriteString(w, `'`)
|
|
||||||
|
io.WriteString(w, `ANY((select a::`)
|
||||||
|
io.WriteString(w, col.Type)
|
||||||
|
|
||||||
|
io.WriteString(w, ` AS list from json_array_elements_text(`)
|
||||||
|
renderPathJSON(w, item, key, k)
|
||||||
|
io.WriteString(w, `::json) AS a))`)
|
||||||
|
|
||||||
|
} else if col.Array {
|
||||||
|
io.WriteString(w, `(`)
|
||||||
|
renderPathJSON(w, item, key, k)
|
||||||
|
io.WriteString(w, `)::`)
|
||||||
|
io.WriteString(w, col.Type)
|
||||||
|
|
||||||
|
io.WriteString(w, ` = ANY(`)
|
||||||
|
colWithTable(w, ti.Name, k)
|
||||||
|
io.WriteString(w, `)`)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
colWithTable(w, ti.Name, k)
|
||||||
|
|
||||||
|
io.WriteString(w, `= (`)
|
||||||
|
renderPathJSON(w, item, key, k)
|
||||||
|
io.WriteString(w, `)::`)
|
||||||
|
io.WriteString(w, col.Type)
|
||||||
|
}
|
||||||
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderPathJSON(w io.Writer, item kvitem, key1, key2 string) {
|
||||||
|
io.WriteString(w, `(i.j->`)
|
||||||
|
joinPath(w, item.path)
|
||||||
|
io.WriteString(w, `->'`)
|
||||||
|
io.WriteString(w, key1)
|
||||||
|
io.WriteString(w, `'->>'`)
|
||||||
|
io.WriteString(w, key2)
|
||||||
|
io.WriteString(w, `')`)
|
||||||
|
}
|
||||||
|
|
||||||
func renderCteName(w io.Writer, item kvitem) error {
|
func renderCteName(w io.Writer, item kvitem) error {
|
||||||
io.WriteString(w, `"`)
|
io.WriteString(w, `"`)
|
||||||
io.WriteString(w, item.ti.Name)
|
io.WriteString(w, item.ti.Name)
|
||||||
|
@ -3,7 +3,6 @@ package psql
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dosco/super-graph/qcode"
|
"github.com/dosco/super-graph/qcode"
|
||||||
@ -128,92 +127,7 @@ func TestMain(m *testing.M) {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tables := []DBTable{
|
schema := getTestSchema()
|
||||||
DBTable{Name: "customers", Type: "table"},
|
|
||||||
DBTable{Name: "users", Type: "table"},
|
|
||||||
DBTable{Name: "products", Type: "table"},
|
|
||||||
DBTable{Name: "purchases", Type: "table"},
|
|
||||||
DBTable{Name: "tags", Type: "table"},
|
|
||||||
}
|
|
||||||
|
|
||||||
columns := [][]DBColumn{
|
|
||||||
[]DBColumn{
|
|
||||||
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
|
||||||
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 4, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 5, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 6, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 7, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 8, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 9, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 10, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
|
|
||||||
[]DBColumn{
|
|
||||||
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
|
||||||
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 4, Name: "avatar", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 5, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 6, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 7, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 8, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 9, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 10, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 11, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
|
|
||||||
[]DBColumn{
|
|
||||||
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
|
||||||
DBColumn{ID: 2, Name: "name", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 3, Name: "description", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 4, Name: "price", Type: "numeric(7,2)", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 5, Name: "user_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "users", FKeyColID: []int16{1}},
|
|
||||||
DBColumn{ID: 6, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 7, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 8, Name: "tsv", Type: "tsvector", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 9, Name: "tags", Type: "text[]", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tags", FKeyColID: []int16{3}, Array: true}},
|
|
||||||
[]DBColumn{
|
|
||||||
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
|
||||||
DBColumn{ID: 2, Name: "customer_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "customers", FKeyColID: []int16{1}},
|
|
||||||
DBColumn{ID: 3, Name: "product_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "products", FKeyColID: []int16{1}},
|
|
||||||
DBColumn{ID: 4, Name: "sale_type", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 5, Name: "quantity", Type: "integer", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 6, Name: "due_date", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 7, Name: "returned", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false}},
|
|
||||||
[]DBColumn{
|
|
||||||
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
|
||||||
DBColumn{ID: 2, Name: "name", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
|
||||||
DBColumn{ID: 3, Name: "slug", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range tables {
|
|
||||||
tables[i].Key = strings.ToLower(tables[i].Name)
|
|
||||||
for n := range columns[i] {
|
|
||||||
columns[i][n].Key = strings.ToLower(columns[i][n].Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema := &DBSchema{
|
|
||||||
ver: 110000,
|
|
||||||
t: make(map[string]*DBTableInfo),
|
|
||||||
rm: make(map[string]map[string]*DBRel),
|
|
||||||
}
|
|
||||||
|
|
||||||
aliases := map[string][]string{
|
|
||||||
"users": []string{"mes"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, t := range tables {
|
|
||||||
err := schema.addTable(t, columns[i], aliases)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, t := range tables {
|
|
||||||
err := schema.updateRelationships(t, columns[i])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := NewVariables(map[string]string{
|
vars := NewVariables(map[string]string{
|
||||||
"admin_account_id": "5",
|
"admin_account_id": "5",
|
||||||
|
192
psql/query.go
192
psql/query.go
@ -82,17 +82,21 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
|||||||
multiRoot := (len(qc.Roots) > 1)
|
multiRoot := (len(qc.Roots) > 1)
|
||||||
|
|
||||||
st := NewIntStack()
|
st := NewIntStack()
|
||||||
|
si := 0
|
||||||
|
|
||||||
if multiRoot {
|
if multiRoot {
|
||||||
io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `)
|
io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `)
|
||||||
|
|
||||||
for i, id := range qc.Roots {
|
for _, id := range qc.Roots {
|
||||||
root := qc.Selects[id]
|
root := qc.Selects[id]
|
||||||
|
if root.SkipRender {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
st.Push(root.ID + closeBlock)
|
st.Push(root.ID + closeBlock)
|
||||||
st.Push(root.ID)
|
st.Push(root.ID)
|
||||||
|
|
||||||
if i != 0 {
|
if si != 0 {
|
||||||
io.WriteString(c.w, `, `)
|
io.WriteString(c.w, `, `)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,13 +107,17 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
|||||||
io.WriteString(c.w, `"`)
|
io.WriteString(c.w, `"`)
|
||||||
|
|
||||||
alias(c.w, root.FieldName)
|
alias(c.w, root.FieldName)
|
||||||
|
si++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if si != 0 {
|
||||||
io.WriteString(c.w, ` FROM `)
|
io.WriteString(c.w, ` FROM `)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
root := qc.Selects[0]
|
root := qc.Selects[0]
|
||||||
|
if !root.SkipRender {
|
||||||
io.WriteString(c.w, `SELECT json_object_agg(`)
|
io.WriteString(c.w, `SELECT json_object_agg(`)
|
||||||
io.WriteString(c.w, `'`)
|
io.WriteString(c.w, `'`)
|
||||||
io.WriteString(c.w, root.FieldName)
|
io.WriteString(c.w, root.FieldName)
|
||||||
@ -121,6 +129,12 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
|||||||
st.Push(root.ID)
|
st.Push(root.ID)
|
||||||
|
|
||||||
io.WriteString(c.w, `) FROM `)
|
io.WriteString(c.w, `) FROM `)
|
||||||
|
si++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if si == 0 {
|
||||||
|
return 0, errors.New("all tables skipped. cannot render query")
|
||||||
}
|
}
|
||||||
|
|
||||||
var ignored uint32
|
var ignored uint32
|
||||||
@ -161,6 +175,9 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
child := &c.s[cid]
|
child := &c.s[cid]
|
||||||
|
if child.SkipRender {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
st.Push(child.ID + closeBlock)
|
st.Push(child.ID + closeBlock)
|
||||||
st.Push(child.ID)
|
st.Push(child.ID)
|
||||||
@ -207,7 +224,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
|||||||
return ignored, nil
|
return ignored, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (uint32, []*qcode.Column) {
|
func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (uint32, []*qcode.Column, error) {
|
||||||
var skipped uint32
|
var skipped uint32
|
||||||
|
|
||||||
cols := make([]*qcode.Column, 0, len(sel.Cols))
|
cols := make([]*qcode.Column, 0, len(sel.Cols))
|
||||||
@ -217,45 +234,72 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
|
|||||||
colmap[sel.Cols[i].Name] = struct{}{}
|
colmap[sel.Cols[i].Name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range sel.OrderBy {
|
||||||
|
colmap[sel.OrderBy[i].Col] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
for _, id := range sel.Children {
|
for _, id := range sel.Children {
|
||||||
child := &c.s[id]
|
child := &c.s[id]
|
||||||
|
|
||||||
rel, err := c.schema.GetRel(child.Name, ti.Name)
|
rel, err := c.schema.GetRel(child.Name, ti.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
skipped |= (1 << uint(id))
|
return 0, nil, err
|
||||||
continue
|
//skipped |= (1 << uint(id))
|
||||||
|
//continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch rel.Type {
|
switch rel.Type {
|
||||||
case RelOneToOne, RelOneToMany:
|
case RelOneToOne, RelOneToMany:
|
||||||
if _, ok := colmap[rel.Right.Col]; !ok {
|
if _, ok := colmap[rel.Right.Col]; !ok {
|
||||||
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Right.Col, FieldName: rel.Right.Col})
|
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Right.Col, FieldName: rel.Right.Col})
|
||||||
}
|
|
||||||
colmap[rel.Right.Col] = struct{}{}
|
colmap[rel.Right.Col] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
case RelOneToManyThrough:
|
case RelOneToManyThrough:
|
||||||
if _, ok := colmap[rel.Left.Col]; !ok {
|
if _, ok := colmap[rel.Left.Col]; !ok {
|
||||||
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Left.Col})
|
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Left.Col})
|
||||||
}
|
|
||||||
colmap[rel.Left.Col] = struct{}{}
|
colmap[rel.Left.Col] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
case RelEmbedded:
|
||||||
|
if _, ok := colmap[rel.Left.Col]; !ok {
|
||||||
|
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Left.Col})
|
||||||
|
colmap[rel.Left.Col] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
case RelRemote:
|
case RelRemote:
|
||||||
if _, ok := colmap[rel.Left.Col]; !ok {
|
if _, ok := colmap[rel.Left.Col]; !ok {
|
||||||
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Right.Col})
|
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Right.Col})
|
||||||
}
|
|
||||||
colmap[rel.Left.Col] = struct{}{}
|
colmap[rel.Left.Col] = struct{}{}
|
||||||
skipped |= (1 << uint(id))
|
skipped |= (1 << uint(id))
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
skipped |= (1 << uint(id))
|
return 0, nil, fmt.Errorf("unknown relationship %s", rel)
|
||||||
|
//skipped |= (1 << uint(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return skipped, cols
|
return skipped, cols, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint32, error) {
|
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint32, error) {
|
||||||
skipped, childCols := c.processChildren(sel, ti)
|
var rel *DBRel
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if sel.ParentID != -1 {
|
||||||
|
parent := c.s[sel.ParentID]
|
||||||
|
|
||||||
|
rel, err = c.schema.GetRel(ti.Name, parent.Name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped, childCols, err := c.processChildren(sel, ti)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
hasOrder := len(sel.OrderBy) != 0
|
hasOrder := len(sel.OrderBy) != 0
|
||||||
|
|
||||||
// SELECT
|
// SELECT
|
||||||
@ -267,9 +311,8 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
|||||||
io.WriteString(c.w, `"`)
|
io.WriteString(c.w, `"`)
|
||||||
|
|
||||||
if hasOrder {
|
if hasOrder {
|
||||||
err := c.renderOrderBy(sel, ti)
|
if err := c.renderOrderBy(sel, ti); err != nil {
|
||||||
if err != nil {
|
return 0, err
|
||||||
return skipped, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,8 +341,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
|||||||
|
|
||||||
c.renderRemoteRelColumns(sel, ti)
|
c.renderRemoteRelColumns(sel, ti)
|
||||||
|
|
||||||
err := c.renderJoinedColumns(sel, ti, skipped)
|
if err = c.renderJoinedColumns(sel, ti, skipped); err != nil {
|
||||||
if err != nil {
|
|
||||||
return skipped, err
|
return skipped, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +360,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
|||||||
// END-SELECT
|
// END-SELECT
|
||||||
|
|
||||||
// FROM (SELECT .... )
|
// FROM (SELECT .... )
|
||||||
err = c.renderBaseSelect(sel, ti, childCols, skipped)
|
err = c.renderBaseSelect(sel, ti, rel, childCols, skipped)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return skipped, err
|
return skipped, err
|
||||||
}
|
}
|
||||||
@ -471,18 +513,22 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error {
|
func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error {
|
||||||
colsRendered := len(sel.Cols) != 0
|
|
||||||
|
// columns previously rendered
|
||||||
|
i := len(sel.Cols)
|
||||||
|
|
||||||
for _, id := range sel.Children {
|
for _, id := range sel.Children {
|
||||||
skipThis := hasBit(skipped, uint32(id))
|
if hasBit(skipped, uint32(id)) {
|
||||||
|
|
||||||
if colsRendered && !skipThis {
|
|
||||||
io.WriteString(c.w, ", ")
|
|
||||||
}
|
|
||||||
if skipThis {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
childSel := &c.s[id]
|
childSel := &c.s[id]
|
||||||
|
if childSel.SkipRender {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != 0 {
|
||||||
|
io.WriteString(c.w, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
|
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
|
||||||
//s.Name, s.ID, s.Name, s.FieldName)
|
//s.Name, s.ID, s.Name, s.FieldName)
|
||||||
@ -496,25 +542,29 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo
|
|||||||
io.WriteString(c.w, `" AS "`)
|
io.WriteString(c.w, `" AS "`)
|
||||||
io.WriteString(c.w, childSel.FieldName)
|
io.WriteString(c.w, childSel.FieldName)
|
||||||
io.WriteString(c.w, `"`)
|
io.WriteString(c.w, `"`)
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, rel *DBRel,
|
||||||
childCols []*qcode.Column, skipped uint32) error {
|
childCols []*qcode.Column, skipped uint32) error {
|
||||||
var groupBy []int
|
var groupBy []int
|
||||||
|
|
||||||
isRoot := sel.ParentID == -1
|
isRoot := (rel == nil)
|
||||||
isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop)
|
isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop)
|
||||||
isSearch := sel.Args["search"] != nil
|
isSearch := sel.Args["search"] != nil
|
||||||
isAgg := false
|
isAgg := false
|
||||||
|
|
||||||
|
colmap := make(map[string]struct{}, (len(sel.Cols) + len(sel.OrderBy)))
|
||||||
|
|
||||||
io.WriteString(c.w, ` FROM (SELECT `)
|
io.WriteString(c.w, ` FROM (SELECT `)
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
for n, col := range sel.Cols {
|
for n, col := range sel.Cols {
|
||||||
cn := col.Name
|
cn := col.Name
|
||||||
|
colmap[cn] = struct{}{}
|
||||||
|
|
||||||
_, isRealCol := ti.ColMap[cn]
|
_, isRealCol := ti.ColMap[cn]
|
||||||
|
|
||||||
@ -625,7 +675,23 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, ob := range sel.OrderBy {
|
||||||
|
if _, ok := colmap[ob.Col]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
colmap[ob.Col] = struct{}{}
|
||||||
|
|
||||||
|
if i != 0 {
|
||||||
|
io.WriteString(c.w, `, `)
|
||||||
|
}
|
||||||
|
colWithTable(c.w, ti.Name, ob.Col)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
for _, col := range childCols {
|
for _, col := range childCols {
|
||||||
|
if _, ok := colmap[col.Name]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
io.WriteString(c.w, `, `)
|
io.WriteString(c.w, `, `)
|
||||||
}
|
}
|
||||||
@ -637,10 +703,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
|||||||
|
|
||||||
io.WriteString(c.w, ` FROM `)
|
io.WriteString(c.w, ` FROM `)
|
||||||
|
|
||||||
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name)
|
c.renderFrom(sel, ti, rel)
|
||||||
io.WriteString(c.w, `"`)
|
|
||||||
io.WriteString(c.w, ti.Name)
|
|
||||||
io.WriteString(c.w, `"`)
|
|
||||||
|
|
||||||
// if tn, ok := c.tmap[sel.Name]; ok {
|
// if tn, ok := c.tmap[sel.Name]; ok {
|
||||||
// //fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Name)
|
// //fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Name)
|
||||||
@ -666,11 +729,9 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(c.w, ` WHERE (`)
|
io.WriteString(c.w, ` WHERE (`)
|
||||||
|
|
||||||
if err := c.renderRelationship(sel, ti); err != nil {
|
if err := c.renderRelationship(sel, ti); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if isFil {
|
if isFil {
|
||||||
io.WriteString(c.w, ` AND `)
|
io.WriteString(c.w, ` AND `)
|
||||||
if err := c.renderWhere(sel, ti); err != nil {
|
if err := c.renderWhere(sel, ti); err != nil {
|
||||||
@ -725,14 +786,50 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInfo) {
|
func (c *compilerContext) renderFrom(sel *qcode.Select, ti *DBTableInfo, rel *DBRel) error {
|
||||||
colsRendered := len(sel.Cols) != 0
|
if rel != nil && rel.Type == RelEmbedded {
|
||||||
|
// json_to_recordset('[{"a":1,"b":[1,2,3],"c":"bar"}, {"a":2,"b":[1,2,3],"c":"bar"}]') as x(a int, b text, d text);
|
||||||
|
|
||||||
for i := range sel.OrderBy {
|
io.WriteString(c.w, `"`)
|
||||||
if colsRendered {
|
io.WriteString(c.w, rel.Left.Table)
|
||||||
//io.WriteString(w, ", ")
|
io.WriteString(c.w, `", `)
|
||||||
|
|
||||||
|
io.WriteString(c.w, ti.Type)
|
||||||
|
io.WriteString(c.w, `_to_recordset(`)
|
||||||
|
colWithTable(c.w, rel.Left.Table, rel.Right.Col)
|
||||||
|
io.WriteString(c.w, `) AS `)
|
||||||
|
|
||||||
|
io.WriteString(c.w, `"`)
|
||||||
|
io.WriteString(c.w, ti.Name)
|
||||||
|
io.WriteString(c.w, `"`)
|
||||||
|
|
||||||
|
io.WriteString(c.w, `(`)
|
||||||
|
for i, col := range ti.Columns {
|
||||||
|
if i != 0 {
|
||||||
io.WriteString(c.w, `, `)
|
io.WriteString(c.w, `, `)
|
||||||
}
|
}
|
||||||
|
io.WriteString(c.w, col.Name)
|
||||||
|
io.WriteString(c.w, ` `)
|
||||||
|
io.WriteString(c.w, col.Type)
|
||||||
|
}
|
||||||
|
io.WriteString(c.w, `)`)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name)
|
||||||
|
io.WriteString(c.w, `"`)
|
||||||
|
io.WriteString(c.w, ti.Name)
|
||||||
|
io.WriteString(c.w, `"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInfo) {
|
||||||
|
//colsRendered := len(sel.Cols) != 0
|
||||||
|
|
||||||
|
for i := range sel.OrderBy {
|
||||||
|
//io.WriteString(w, ", ")
|
||||||
|
io.WriteString(c.w, `, `)
|
||||||
|
|
||||||
col := sel.OrderBy[i].Col
|
col := sel.OrderBy[i].Col
|
||||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`,
|
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`,
|
||||||
@ -807,7 +904,13 @@ func (c *compilerContext) renderRelationshipByName(table, parent string, id int3
|
|||||||
io.WriteString(c.w, `) = (`)
|
io.WriteString(c.w, `) = (`)
|
||||||
colWithTable(c.w, rel.Through, rel.Right.Col)
|
colWithTable(c.w, rel.Through, rel.Right.Col)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case RelEmbedded:
|
||||||
|
colWithTable(c.w, rel.Left.Table, rel.Left.Col)
|
||||||
|
io.WriteString(c.w, `) = (`)
|
||||||
|
colWithTableID(c.w, parent, id, rel.Left.Col)
|
||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(c.w, `))`)
|
io.WriteString(c.w, `))`)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -932,7 +1035,6 @@ func (c *compilerContext) renderNestedWhere(ex *qcode.Exp, ti *DBTableInfo) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Println(">", ex)
|
|
||||||
io.WriteString(c.w, `)`)
|
io.WriteString(c.w, `)`)
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1063,27 +1165,27 @@ func (c *compilerContext) renderOrderBy(sel *qcode.Select, ti *DBTableInfo) erro
|
|||||||
switch ob.Order {
|
switch ob.Order {
|
||||||
case qcode.OrderAsc:
|
case qcode.OrderAsc:
|
||||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Name, sel.ID, ob.Col)
|
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Name, sel.ID, ob.Col)
|
||||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob")
|
||||||
io.WriteString(c.w, ` ASC`)
|
io.WriteString(c.w, ` ASC`)
|
||||||
case qcode.OrderDesc:
|
case qcode.OrderDesc:
|
||||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" DESC`, sel.Name, sel.ID, ob.Col)
|
//fmt.Fprintf(w, `"%s_%d.ob.%s" DESC`, sel.Name, sel.ID, ob.Col)
|
||||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob")
|
||||||
io.WriteString(c.w, ` DESC`)
|
io.WriteString(c.w, ` DESC`)
|
||||||
case qcode.OrderAscNullsFirst:
|
case qcode.OrderAscNullsFirst:
|
||||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC NULLS FIRST`, sel.Name, sel.ID, ob.Col)
|
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC NULLS FIRST`, sel.Name, sel.ID, ob.Col)
|
||||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob")
|
||||||
io.WriteString(c.w, ` ASC NULLS FIRST`)
|
io.WriteString(c.w, ` ASC NULLS FIRST`)
|
||||||
case qcode.OrderDescNullsFirst:
|
case qcode.OrderDescNullsFirst:
|
||||||
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS FIRST`, sel.Name, sel.ID, ob.Col)
|
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS FIRST`, sel.Name, sel.ID, ob.Col)
|
||||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob")
|
||||||
io.WriteString(c.w, ` DESC NULLLS FIRST`)
|
io.WriteString(c.w, ` DESC NULLLS FIRST`)
|
||||||
case qcode.OrderAscNullsLast:
|
case qcode.OrderAscNullsLast:
|
||||||
//fmt.Fprintf(w, `"%s_%d.ob.%s ASC NULLS LAST`, sel.Name, sel.ID, ob.Col)
|
//fmt.Fprintf(w, `"%s_%d.ob.%s ASC NULLS LAST`, sel.Name, sel.ID, ob.Col)
|
||||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob")
|
||||||
io.WriteString(c.w, ` ASC NULLS LAST`)
|
io.WriteString(c.w, ` ASC NULLS LAST`)
|
||||||
case qcode.OrderDescNullsLast:
|
case qcode.OrderDescNullsLast:
|
||||||
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Name, sel.ID, ob.Col)
|
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Name, sel.ID, ob.Col)
|
||||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob")
|
||||||
io.WriteString(c.w, ` DESC NULLS LAST`)
|
io.WriteString(c.w, ` DESC NULLS LAST`)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("13: unexpected value %v", ob.Order)
|
return fmt.Errorf("13: unexpected value %v", ob.Order)
|
||||||
|
@ -209,7 +209,8 @@ func oneToManyReverse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func oneToManyArray(t *testing.T) {
|
func oneToManyArray(t *testing.T) {
|
||||||
gql := `query {
|
gql := `
|
||||||
|
query {
|
||||||
product {
|
product {
|
||||||
name
|
name
|
||||||
price
|
price
|
||||||
@ -224,7 +225,6 @@ func oneToManyArray(t *testing.T) {
|
|||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `SELECT row_to_json("json_root") FROM (SELECT "sel_0"."json_0" AS "tags", "sel_2"."json_2" AS "product" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "products_2"."name" AS "name", "products_2"."price" AS "price", "tags_3_join"."json_3" AS "tags") AS "json_row_2")) AS "json_2" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_3"), '[]') AS "json_3" FROM (SELECT row_to_json((SELECT "json_row_3" FROM (SELECT "tags_3"."id" AS "id", "tags_3"."name" AS "name") AS "json_row_3")) AS "json_3" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3" LIMIT ('20') :: integer) AS "json_agg_3") AS "tags_3_join" ON ('true') LIMIT ('1') :: integer) AS "sel_2", (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "tags_0"."name" AS "name", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0") AS "json_root"`
|
sql := `SELECT row_to_json("json_root") FROM (SELECT "sel_0"."json_0" AS "tags", "sel_2"."json_2" AS "product" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "products_2"."name" AS "name", "products_2"."price" AS "price", "tags_3_join"."json_3" AS "tags") AS "json_row_2")) AS "json_2" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_3"), '[]') AS "json_3" FROM (SELECT row_to_json((SELECT "json_row_3" FROM (SELECT "tags_3"."id" AS "id", "tags_3"."name" AS "name") AS "json_row_3")) AS "json_3" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3" LIMIT ('20') :: integer) AS "json_agg_3") AS "tags_3_join" ON ('true') LIMIT ('1') :: integer) AS "sel_2", (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "tags_0"."name" AS "name", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0") AS "json_root"`
|
||||||
@ -463,6 +463,56 @@ func multiRoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func jsonColumnAsTable(t *testing.T) {
|
||||||
|
gql := `query {
|
||||||
|
products {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
tag_count {
|
||||||
|
count
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "tag_count_1_join"."json_1" AS "tag_count") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "tag_count_1"."count" AS "count", "tags_2_join"."json_2" AS "tags") AS "json_row_1")) AS "json_1" FROM (SELECT "tag_count"."count", "tag_count"."tag_id" FROM "products", json_to_recordset("products"."tag_count") AS "tag_count"(tag_id bigint, count int) WHERE ((("products"."id") = ("products_0"."id"))) LIMIT ('1') :: integer) AS "tag_count_1" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_2"), '[]') AS "json_2" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "tags_2"."name" AS "name") AS "json_row_2")) AS "json_2" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2" LIMIT ('20') :: integer) AS "json_agg_2") AS "tags_2_join" ON ('true') LIMIT ('1') :: integer) AS "tag_count_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||||
|
|
||||||
|
resSQL, err := compileGQLToPSQL(gql, nil, "admin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(resSQL) != sql {
|
||||||
|
t.Fatal(errNotExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipUserIDForAnonRole(t *testing.T) {
|
||||||
|
gql := `query {
|
||||||
|
products {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
user(where: { id: { eq: $user_id } }) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||||
|
|
||||||
|
resSQL, err := compileGQLToPSQL(gql, nil, "anon")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(resSQL) != sql {
|
||||||
|
t.Fatal(errNotExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func blockedQuery(t *testing.T) {
|
func blockedQuery(t *testing.T) {
|
||||||
gql := `query {
|
gql := `query {
|
||||||
user(id: 5, where: { id: { gt: 3 } }) {
|
user(id: 5, where: { id: { gt: 3 } }) {
|
||||||
@ -524,6 +574,8 @@ func TestCompileQuery(t *testing.T) {
|
|||||||
t.Run("queryWithVariables", queryWithVariables)
|
t.Run("queryWithVariables", queryWithVariables)
|
||||||
t.Run("withWhereOnRelations", withWhereOnRelations)
|
t.Run("withWhereOnRelations", withWhereOnRelations)
|
||||||
t.Run("multiRoot", multiRoot)
|
t.Run("multiRoot", multiRoot)
|
||||||
|
t.Run("jsonColumnAsTable", jsonColumnAsTable)
|
||||||
|
t.Run("skipUserIDForAnonRole", skipUserIDForAnonRole)
|
||||||
t.Run("blockedQuery", blockedQuery)
|
t.Run("blockedQuery", blockedQuery)
|
||||||
t.Run("blockedFunctions", blockedFunctions)
|
t.Run("blockedFunctions", blockedFunctions)
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gobuffalo/flect"
|
"github.com/gobuffalo/flect"
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DBSchema struct {
|
type DBSchema struct {
|
||||||
@ -16,6 +15,7 @@ type DBSchema struct {
|
|||||||
|
|
||||||
type DBTableInfo struct {
|
type DBTableInfo struct {
|
||||||
Name string
|
Name string
|
||||||
|
Type string
|
||||||
Singular bool
|
Singular bool
|
||||||
Columns []DBColumn
|
Columns []DBColumn
|
||||||
PrimaryCol *DBColumn
|
PrimaryCol *DBColumn
|
||||||
@ -30,6 +30,7 @@ const (
|
|||||||
RelOneToOne RelType = iota + 1
|
RelOneToOne RelType = iota + 1
|
||||||
RelOneToMany
|
RelOneToMany
|
||||||
RelOneToManyThrough
|
RelOneToManyThrough
|
||||||
|
RelEmbedded
|
||||||
RelRemote
|
RelRemote
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,9 +52,7 @@ type DBRel struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDBSchema(db *pgxpool.Pool,
|
func NewDBSchema(info *DBInfo, aliases map[string][]string) (*DBSchema, error) {
|
||||||
info *DBInfo, aliases map[string][]string) (*DBSchema, error) {
|
|
||||||
|
|
||||||
schema := &DBSchema{
|
schema := &DBSchema{
|
||||||
t: make(map[string]*DBTableInfo),
|
t: make(map[string]*DBTableInfo),
|
||||||
rm: make(map[string]map[string]*DBRel),
|
rm: make(map[string]map[string]*DBRel),
|
||||||
@ -85,6 +84,7 @@ func (s *DBSchema) addTable(
|
|||||||
singular := flect.Singularize(t.Key)
|
singular := flect.Singularize(t.Key)
|
||||||
s.t[singular] = &DBTableInfo{
|
s.t[singular] = &DBTableInfo{
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
|
Type: t.Type,
|
||||||
Singular: true,
|
Singular: true,
|
||||||
Columns: cols,
|
Columns: cols,
|
||||||
ColMap: colmap,
|
ColMap: colmap,
|
||||||
@ -94,6 +94,7 @@ func (s *DBSchema) addTable(
|
|||||||
plural := flect.Pluralize(t.Key)
|
plural := flect.Pluralize(t.Key)
|
||||||
s.t[plural] = &DBTableInfo{
|
s.t[plural] = &DBTableInfo{
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
|
Type: t.Type,
|
||||||
Singular: false,
|
Singular: false,
|
||||||
Columns: cols,
|
Columns: cols,
|
||||||
ColMap: colmap,
|
ColMap: colmap,
|
||||||
@ -138,20 +139,46 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
|
|||||||
return fmt.Errorf("invalid foreign key table '%s'", ct)
|
return fmt.Errorf("invalid foreign key table '%s'", ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cols {
|
for i := range cols {
|
||||||
if len(c.FKeyTable) == 0 || len(c.FKeyColID) == 0 {
|
c := cols[i]
|
||||||
|
|
||||||
|
if len(c.FKeyTable) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Foreign key column name
|
// Foreign key column name
|
||||||
ft := strings.ToLower(c.FKeyTable)
|
ft := strings.ToLower(c.FKeyTable)
|
||||||
fcid := c.FKeyColID[0]
|
|
||||||
|
|
||||||
ti, ok := s.t[ft]
|
ti, ok := s.t[ft]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid foreign key table '%s'", ft)
|
return fmt.Errorf("invalid foreign key table '%s'", ft)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is an embedded relationship like when a json/jsonb column
|
||||||
|
// is exposed as a table
|
||||||
|
if c.Name == c.FKeyTable && len(c.FKeyColID) == 0 {
|
||||||
|
rel := &DBRel{Type: RelEmbedded}
|
||||||
|
rel.Left.col = cti.PrimaryCol
|
||||||
|
rel.Left.Table = cti.Name
|
||||||
|
rel.Left.Col = cti.PrimaryCol.Name
|
||||||
|
|
||||||
|
rel.Right.col = &c
|
||||||
|
rel.Right.Table = ti.Name
|
||||||
|
rel.Right.Col = c.Name
|
||||||
|
|
||||||
|
if err := s.SetRel(ft, ct, rel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.FKeyColID) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foreign key column id
|
||||||
|
fcid := c.FKeyColID[0]
|
||||||
|
|
||||||
fc, ok := ti.ColIDMap[fcid]
|
fc, ok := ti.ColIDMap[fcid]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid foreign key column id '%d' for table '%s'",
|
return fmt.Errorf("invalid foreign key column id '%d' for table '%s'",
|
||||||
@ -190,10 +217,12 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
|
|||||||
rel2 = &DBRel{Type: RelOneToMany}
|
rel2 = &DBRel{Type: RelOneToMany}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rel2.Left.col = fc
|
||||||
rel2.Left.Table = c.FKeyTable
|
rel2.Left.Table = c.FKeyTable
|
||||||
rel2.Left.Col = fc.Name
|
rel2.Left.Col = fc.Name
|
||||||
rel2.Left.Array = fc.Array
|
rel2.Left.Array = fc.Array
|
||||||
|
|
||||||
|
rel2.Right.col = &c
|
||||||
rel2.Right.Table = t.Name
|
rel2.Right.Table = t.Name
|
||||||
rel2.Right.Col = c.Name
|
rel2.Right.Col = c.Name
|
||||||
rel2.Right.Array = c.Array
|
rel2.Right.Array = c.Array
|
||||||
@ -251,9 +280,11 @@ func (s *DBSchema) updateSchemaOTMT(
|
|||||||
rel1.Through = ti.Name
|
rel1.Through = ti.Name
|
||||||
rel1.ColT = col2.Name
|
rel1.ColT = col2.Name
|
||||||
|
|
||||||
|
rel1.Left.col = &col2
|
||||||
rel1.Left.Table = col2.FKeyTable
|
rel1.Left.Table = col2.FKeyTable
|
||||||
rel1.Left.Col = fc2.Name
|
rel1.Left.Col = fc2.Name
|
||||||
|
|
||||||
|
rel1.Right.col = &col1
|
||||||
rel1.Right.Table = ti.Name
|
rel1.Right.Table = ti.Name
|
||||||
rel1.Right.Col = col1.Name
|
rel1.Right.Col = col1.Name
|
||||||
|
|
||||||
@ -267,9 +298,11 @@ func (s *DBSchema) updateSchemaOTMT(
|
|||||||
rel2.Through = ti.Name
|
rel2.Through = ti.Name
|
||||||
rel2.ColT = col1.Name
|
rel2.ColT = col1.Name
|
||||||
|
|
||||||
|
rel1.Left.col = fc1
|
||||||
rel2.Left.Table = col1.FKeyTable
|
rel2.Left.Table = col1.FKeyTable
|
||||||
rel2.Left.Col = fc1.Name
|
rel2.Left.Col = fc1.Name
|
||||||
|
|
||||||
|
rel1.Right.col = &col2
|
||||||
rel2.Right.Table = ti.Name
|
rel2.Right.Table = ti.Name
|
||||||
rel2.Right.Col = col2.Name
|
rel2.Right.Col = col2.Name
|
||||||
|
|
||||||
@ -321,9 +354,22 @@ func (s *DBSchema) SetRel(child, parent string, rel *DBRel) error {
|
|||||||
|
|
||||||
func (s *DBSchema) GetRel(child, parent string) (*DBRel, error) {
|
func (s *DBSchema) GetRel(child, parent string) (*DBRel, error) {
|
||||||
rel, ok := s.rm[child][parent]
|
rel, ok := s.rm[child][parent]
|
||||||
|
if !ok {
|
||||||
|
// No relationship found so this time fetch the table info
|
||||||
|
// and try again in case child or parent was an alias
|
||||||
|
ct, err := s.GetTable(child)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pt, err := s.GetTable(parent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rel, ok = s.rm[ct.Name][pt.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unknown relationship '%s' -> '%s'",
|
return nil, fmt.Errorf("unknown relationship '%s' -> '%s'",
|
||||||
child, parent)
|
child, parent)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return rel, nil
|
return rel, nil
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ func (rt RelType) String() string {
|
|||||||
return "one to many through"
|
return "one to many through"
|
||||||
case RelRemote:
|
case RelRemote:
|
||||||
return "remote"
|
return "remote"
|
||||||
|
case RelEmbedded:
|
||||||
|
return "embedded"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,20 @@ func GetDBInfo(db *pgxpool.Pool) (*DBInfo, error) {
|
|||||||
return di, nil
|
return di, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (di *DBInfo) AddTable(t DBTable, cols []DBColumn) {
|
||||||
|
t.ID = di.Tables[len(di.Tables)-1].ID
|
||||||
|
|
||||||
|
di.Tables = append(di.Tables, t)
|
||||||
|
di.colmap[t.Key] = make(map[string]*DBColumn, len(cols))
|
||||||
|
|
||||||
|
for i := range cols {
|
||||||
|
cols[i].ID = int16(i)
|
||||||
|
c := &cols[i]
|
||||||
|
di.colmap[t.Key][c.Key] = c
|
||||||
|
}
|
||||||
|
di.Columns = append(di.Columns, cols)
|
||||||
|
}
|
||||||
|
|
||||||
func (di *DBInfo) GetColumn(table, column string) (*DBColumn, bool) {
|
func (di *DBInfo) GetColumn(table, column string) (*DBColumn, bool) {
|
||||||
v, ok := di.colmap[strings.ToLower(table)][strings.ToLower(column)]
|
v, ok := di.colmap[strings.ToLower(table)][strings.ToLower(column)]
|
||||||
return v, ok
|
return v, ok
|
||||||
@ -137,6 +151,7 @@ SELECT
|
|||||||
pg_catalog.format_type(f.atttypid,f.atttypmod) AS type,
|
pg_catalog.format_type(f.atttypid,f.atttypmod) AS type,
|
||||||
CASE
|
CASE
|
||||||
WHEN f.attndims != 0 THEN true
|
WHEN f.attndims != 0 THEN true
|
||||||
|
WHEN right(pg_catalog.format_type(f.atttypid,f.atttypmod), 2) = '[]' THEN true
|
||||||
ELSE false
|
ELSE false
|
||||||
END AS array,
|
END AS array,
|
||||||
CASE
|
CASE
|
||||||
@ -161,7 +176,7 @@ FROM pg_attribute f
|
|||||||
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
|
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
|
||||||
LEFT JOIN pg_class AS g ON p.confrelid = g.oid
|
LEFT JOIN pg_class AS g ON p.confrelid = g.oid
|
||||||
WHERE c.relkind = ('r'::char)
|
WHERE c.relkind IN ('r', 'v', 'm', 'f')
|
||||||
AND n.nspname = $1 -- Replace with Schema name
|
AND n.nspname = $1 -- Replace with Schema name
|
||||||
AND c.relname = $2 -- Replace with table name
|
AND c.relname = $2 -- Replace with table name
|
||||||
AND f.attnum > 0
|
AND f.attnum > 0
|
||||||
|
102
psql/test_schema.go
Normal file
102
psql/test_schema.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package psql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTestSchema() *DBSchema {
|
||||||
|
tables := []DBTable{
|
||||||
|
DBTable{Name: "customers", Type: "table"},
|
||||||
|
DBTable{Name: "users", Type: "table"},
|
||||||
|
DBTable{Name: "products", Type: "table"},
|
||||||
|
DBTable{Name: "purchases", Type: "table"},
|
||||||
|
DBTable{Name: "tags", Type: "table"},
|
||||||
|
DBTable{Name: "tag_count", Type: "json"},
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := [][]DBColumn{
|
||||||
|
[]DBColumn{
|
||||||
|
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
||||||
|
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 4, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 5, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 6, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 7, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 8, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 9, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 10, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
|
||||||
|
[]DBColumn{
|
||||||
|
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
||||||
|
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 4, Name: "avatar", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 5, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 6, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 7, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 8, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 9, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 10, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 11, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
|
||||||
|
[]DBColumn{
|
||||||
|
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
||||||
|
DBColumn{ID: 2, Name: "name", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 3, Name: "description", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 4, Name: "price", Type: "numeric(7,2)", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 5, Name: "user_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "users", FKeyColID: []int16{1}},
|
||||||
|
DBColumn{ID: 6, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 7, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 8, Name: "tsv", Type: "tsvector", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 9, Name: "tags", Type: "text[]", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tags", FKeyColID: []int16{3}, Array: true},
|
||||||
|
DBColumn{ID: 9, Name: "tag_count", Type: "json", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tag_count", FKeyColID: []int16{}}},
|
||||||
|
[]DBColumn{
|
||||||
|
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
||||||
|
DBColumn{ID: 2, Name: "customer_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "customers", FKeyColID: []int16{1}},
|
||||||
|
DBColumn{ID: 3, Name: "product_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "products", FKeyColID: []int16{1}},
|
||||||
|
DBColumn{ID: 4, Name: "sale_type", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 5, Name: "quantity", Type: "integer", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 6, Name: "due_date", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 7, Name: "returned", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false}},
|
||||||
|
[]DBColumn{
|
||||||
|
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
|
||||||
|
DBColumn{ID: 2, Name: "name", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
|
||||||
|
DBColumn{ID: 3, Name: "slug", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false}},
|
||||||
|
[]DBColumn{
|
||||||
|
DBColumn{ID: 1, Name: "tag_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tags", FKeyColID: []int16{1}},
|
||||||
|
DBColumn{ID: 2, Name: "count", Type: "int", NotNull: false, PrimaryKey: false, UniqueKey: false}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tables {
|
||||||
|
tables[i].Key = strings.ToLower(tables[i].Name)
|
||||||
|
for n := range columns[i] {
|
||||||
|
columns[i][n].Key = strings.ToLower(columns[i][n].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := &DBSchema{
|
||||||
|
ver: 110000,
|
||||||
|
t: make(map[string]*DBTableInfo),
|
||||||
|
rm: make(map[string]map[string]*DBRel),
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases := map[string][]string{
|
||||||
|
"users": []string{"mes"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, t := range tables {
|
||||||
|
err := schema.addTable(t, columns[i], aliases)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, t := range tables {
|
||||||
|
err := schema.updateRelationships(t, columns[i])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema
|
||||||
|
}
|
@ -15,7 +15,10 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer,
|
|||||||
|
|
||||||
update, ok := vars[qc.ActionVar]
|
update, ok := vars[qc.ActionVar]
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, fmt.Errorf("Variable '%s' not !defined", qc.ActionVar)
|
return 0, fmt.Errorf("variable '%s' not !defined", qc.ActionVar)
|
||||||
|
}
|
||||||
|
if len(update) == 0 {
|
||||||
|
return 0, fmt.Errorf("variable '%s' is empty", qc.ActionVar)
|
||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
|
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
|
||||||
@ -125,16 +128,16 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re
|
|||||||
if item.relPC.Type == RelOneToMany {
|
if item.relPC.Type == RelOneToMany {
|
||||||
if conn, ok := item.data["where"]; ok {
|
if conn, ok := item.data["where"]; ok {
|
||||||
io.WriteString(w, ` AND `)
|
io.WriteString(w, ` AND `)
|
||||||
renderWhereFromJSON(w, item.ti.Name, conn)
|
renderWhereFromJSON(w, item.kvitem, "where", conn)
|
||||||
} else if conn, ok := item.data["_where"]; ok {
|
} else if conn, ok := item.data["_where"]; ok {
|
||||||
io.WriteString(w, ` AND `)
|
io.WriteString(w, ` AND `)
|
||||||
renderWhereFromJSON(w, item.ti.Name, conn)
|
renderWhereFromJSON(w, item.kvitem, "_where", conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
io.WriteString(w, `)`)
|
io.WriteString(w, `)`)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
io.WriteString(w, `WHERE `)
|
io.WriteString(w, ` WHERE `)
|
||||||
if err := c.renderWhere(&qc.Selects[0], ti); err != nil {
|
if err := c.renderWhere(&qc.Selects[0], ti); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -165,9 +168,28 @@ func renderNestedUpdateRelColumns(w io.Writer, item kvitem, values bool) error {
|
|||||||
for _, v := range item.items {
|
for _, v := range item.items {
|
||||||
if v._ctype > 0 && v.relCP.Type == RelOneToMany {
|
if v._ctype > 0 && v.relCP.Type == RelOneToMany {
|
||||||
if values {
|
if values {
|
||||||
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col)
|
// if v.relCP.Right.Array {
|
||||||
|
// io.WriteString(w, `array_diff(`)
|
||||||
|
// colWithTable(w, v.relCP.Right.Table, v.relCP.Right.Col)
|
||||||
|
// io.WriteString(w, `, `)
|
||||||
|
// }
|
||||||
|
|
||||||
|
if v._ctype > 0 {
|
||||||
|
io.WriteString(w, `"_x_`)
|
||||||
|
io.WriteString(w, v.relCP.Left.Table)
|
||||||
|
io.WriteString(w, `".`)
|
||||||
|
quoted(w, v.relCP.Left.Col)
|
||||||
} else {
|
} else {
|
||||||
|
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if v.relCP.Right.Array {
|
||||||
|
// io.WriteString(w, `)`)
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
|
||||||
quoted(w, v.relCP.Right.Col)
|
quoted(w, v.relCP.Right.Col)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,12 +198,13 @@ func renderNestedUpdateRelColumns(w io.Writer, item kvitem, values bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderNestedUpdateRelTables(w io.Writer, item kvitem) error {
|
func renderNestedUpdateRelTables(w io.Writer, item kvitem) error {
|
||||||
// Render child foreign key columns if child-to-parent
|
// Render tables needed to set values if child-to-parent
|
||||||
// relationship is one-to-many
|
// relationship is one-to-many
|
||||||
for _, v := range item.items {
|
for _, v := range item.items {
|
||||||
if v._ctype > 0 && v.relCP.Type == RelOneToMany {
|
if v._ctype > 0 && v.relCP.Type == RelOneToMany {
|
||||||
quoted(w, v.relCP.Left.Table)
|
io.WriteString(w, `"_x_`)
|
||||||
io.WriteString(w, `, `)
|
io.WriteString(w, v.relCP.Left.Table)
|
||||||
|
io.WriteString(w, `", `)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ func singleUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE ((("products"."id") IS NOT DISTINCT FROM 1) AND (("products"."id") = 15)) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
sql := `WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE ((("products"."id") IS NOT DISTINCT FROM 1) AND (("products"."id") = 15)) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
|
"update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
|
||||||
@ -36,7 +36,7 @@ func simpleUpdateWithPresets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE (("products"."user_id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),
|
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),
|
||||||
@ -71,9 +71,9 @@ func nestedUpdateManyToMany(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t)WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t)WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(` {
|
"data": json.RawMessage(` {
|
||||||
@ -119,7 +119,7 @@ func nestedUpdateOneToMany(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t)WHERE (("users"."id") IS NOT DISTINCT FROM 8) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id" = '2') RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") IS NOT DISTINCT FROM 8) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id"= ((i.j->'product'->'where'->>'id'))::bigint) RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
@ -162,7 +162,7 @@ func nestedUpdateOneToOne(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 6) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 6) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
@ -200,7 +200,7 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t)WHERE (("users"."id") = 6) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id"FROM "users" WHERE ("products"."id" = '7') RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id" = '8') RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") = 6) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id"= ((i.j->'product'->'disconnect'->>'id'))::bigint) RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
@ -238,9 +238,9 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."id" = '5' AND "users"."email" = 'test@test.com' LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint AND "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."email" = 'test@test.com' AND "users"."id" = '5' LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying AND "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
@ -273,7 +273,7 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 2) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 2) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
vars := map[string]json.RawMessage{
|
vars := map[string]json.RawMessage{
|
||||||
"data": json.RawMessage(`{
|
"data": json.RawMessage(`{
|
||||||
@ -295,6 +295,37 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// func nestedUpdateOneToOneWithDisconnectArray(t *testing.T) {
|
||||||
|
// gql := `mutation {
|
||||||
|
// product(update: $data, id: 2) {
|
||||||
|
// id
|
||||||
|
// name
|
||||||
|
// user_id
|
||||||
|
// }
|
||||||
|
// }`
|
||||||
|
|
||||||
|
// sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 2) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||||
|
|
||||||
|
// vars := map[string]json.RawMessage{
|
||||||
|
// "data": json.RawMessage(`{
|
||||||
|
// "name": "Apple",
|
||||||
|
// "price": 1.25,
|
||||||
|
// "user": {
|
||||||
|
// "disconnect": { "id": 5 }
|
||||||
|
// }
|
||||||
|
// }`),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if string(resSQL) != sql {
|
||||||
|
// t.Fatal(errNotExpected)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
func TestCompileUpdate(t *testing.T) {
|
func TestCompileUpdate(t *testing.T) {
|
||||||
t.Run("singleUpdate", singleUpdate)
|
t.Run("singleUpdate", singleUpdate)
|
||||||
t.Run("simpleUpdateWithPresets", simpleUpdateWithPresets)
|
t.Run("simpleUpdateWithPresets", simpleUpdateWithPresets)
|
||||||
@ -304,4 +335,5 @@ func TestCompileUpdate(t *testing.T) {
|
|||||||
t.Run("nestedUpdateOneToManyWithConnect", nestedUpdateOneToManyWithConnect)
|
t.Run("nestedUpdateOneToManyWithConnect", nestedUpdateOneToManyWithConnect)
|
||||||
t.Run("nestedUpdateOneToOneWithConnect", nestedUpdateOneToOneWithConnect)
|
t.Run("nestedUpdateOneToOneWithConnect", nestedUpdateOneToOneWithConnect)
|
||||||
t.Run("nestedUpdateOneToOneWithDisconnect", nestedUpdateOneToOneWithDisconnect)
|
t.Run("nestedUpdateOneToOneWithDisconnect", nestedUpdateOneToOneWithDisconnect)
|
||||||
|
//t.Run("nestedUpdateOneToOneWithDisconnectArray", nestedUpdateOneToOneWithDisconnectArray)
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@ type trval struct {
|
|||||||
query struct {
|
query struct {
|
||||||
limit string
|
limit string
|
||||||
fil *Exp
|
fil *Exp
|
||||||
|
filNU bool
|
||||||
cols map[string]struct{}
|
cols map[string]struct{}
|
||||||
disable struct {
|
disable struct {
|
||||||
funcs bool
|
funcs bool
|
||||||
@ -53,6 +54,7 @@ type trval struct {
|
|||||||
|
|
||||||
insert struct {
|
insert struct {
|
||||||
fil *Exp
|
fil *Exp
|
||||||
|
filNU bool
|
||||||
cols map[string]struct{}
|
cols map[string]struct{}
|
||||||
psmap map[string]string
|
psmap map[string]string
|
||||||
pslist []string
|
pslist []string
|
||||||
@ -60,6 +62,7 @@ type trval struct {
|
|||||||
|
|
||||||
update struct {
|
update struct {
|
||||||
fil *Exp
|
fil *Exp
|
||||||
|
filNU bool
|
||||||
cols map[string]struct{}
|
cols map[string]struct{}
|
||||||
psmap map[string]string
|
psmap map[string]string
|
||||||
pslist []string
|
pslist []string
|
||||||
@ -67,6 +70,7 @@ type trval struct {
|
|||||||
|
|
||||||
delete struct {
|
delete struct {
|
||||||
fil *Exp
|
fil *Exp
|
||||||
|
filNU bool
|
||||||
cols map[string]struct{}
|
cols map[string]struct{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,21 +92,21 @@ func (trv *trval) allowedColumns(qt QType) map[string]struct{} {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (trv *trval) filter(qt QType) *Exp {
|
func (trv *trval) filter(qt QType) (*Exp, bool) {
|
||||||
switch qt {
|
switch qt {
|
||||||
case QTQuery:
|
case QTQuery:
|
||||||
return trv.query.fil
|
return trv.query.fil, trv.query.filNU
|
||||||
case QTInsert:
|
case QTInsert:
|
||||||
return trv.insert.fil
|
return trv.insert.fil, trv.insert.filNU
|
||||||
case QTUpdate:
|
case QTUpdate:
|
||||||
return trv.update.fil
|
return trv.update.fil, trv.update.filNU
|
||||||
case QTDelete:
|
case QTDelete:
|
||||||
return trv.delete.fil
|
return trv.delete.fil, trv.delete.filNU
|
||||||
case QTUpsert:
|
case QTUpsert:
|
||||||
return trv.insert.fil
|
return trv.insert.fil, trv.insert.filNU
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func listToMap(list []string) map[string]struct{} {
|
func listToMap(list []string) map[string]struct{} {
|
||||||
|
@ -4,7 +4,11 @@ package qcode
|
|||||||
|
|
||||||
// FuzzerEntrypoint for Fuzzbuzz
|
// FuzzerEntrypoint for Fuzzbuzz
|
||||||
func Fuzz(data []byte) int {
|
func Fuzz(data []byte) int {
|
||||||
GetQType(string(data))
|
qt := GetQType(string(data))
|
||||||
|
|
||||||
|
if qt > QTUpsert {
|
||||||
|
panic("qt > QTUpsert")
|
||||||
|
}
|
||||||
|
|
||||||
qcompile, _ := NewCompiler(Config{})
|
qcompile, _ := NewCompiler(Config{})
|
||||||
_, err := qcompile.Compile(data, "user")
|
_, err := qcompile.Compile(data, "user")
|
||||||
|
13
qcode/lex.go
13
qcode/lex.go
@ -31,7 +31,7 @@ type item struct {
|
|||||||
_type itemType // The type of this item.
|
_type itemType // The type of this item.
|
||||||
pos Pos // The starting position, in bytes, of this item in the input string.
|
pos Pos // The starting position, in bytes, of this item in the input string.
|
||||||
end Pos // The ending position, in bytes, of this item in the input string.
|
end Pos // The ending position, in bytes, of this item in the input string.
|
||||||
line uint16 // The line number at the start of this item.
|
line int16 // The line number at the start of this item.
|
||||||
}
|
}
|
||||||
|
|
||||||
// itemType identifies the type of lex items.
|
// itemType identifies the type of lex items.
|
||||||
@ -87,7 +87,7 @@ type lexer struct {
|
|||||||
width Pos // width of last rune read from input
|
width Pos // width of last rune read from input
|
||||||
items []item // array of scanned items
|
items []item // array of scanned items
|
||||||
itemsA [50]item
|
itemsA [50]item
|
||||||
line uint16 // 1+number of newlines seen
|
line int16 // 1+number of newlines seen
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ func (l *lexer) emit(t itemType) {
|
|||||||
l.items = append(l.items, item{t, l.start, l.pos, l.line})
|
l.items = append(l.items, item{t, l.start, l.pos, l.line})
|
||||||
// Some items contain text internally. If so, count their newlines.
|
// Some items contain text internally. If so, count their newlines.
|
||||||
switch t {
|
switch t {
|
||||||
case itemName:
|
case itemStringVal:
|
||||||
for i := l.start; i < l.pos; i++ {
|
for i := l.start; i < l.pos; i++ {
|
||||||
if l.input[i] == '\n' {
|
if l.input[i] == '\n' {
|
||||||
l.line++
|
l.line++
|
||||||
@ -155,11 +155,6 @@ func (l *lexer) emitL(t itemType) {
|
|||||||
|
|
||||||
// ignore skips over the pending input before this point.
|
// ignore skips over the pending input before this point.
|
||||||
func (l *lexer) ignore() {
|
func (l *lexer) ignore() {
|
||||||
for i := l.start; i < l.pos; i++ {
|
|
||||||
if l.input[i] == '\n' {
|
|
||||||
l.line++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
l.start = l.pos
|
l.start = l.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,7 +431,7 @@ func lowercase(b []byte, s Pos, e Pos) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *item) String() string {
|
func (i item) String() string {
|
||||||
var v string
|
var v string
|
||||||
|
|
||||||
switch i._type {
|
switch i._type {
|
||||||
|
@ -156,12 +156,19 @@ func parseSelectionSet(gql []byte) (*Operation, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lexPool.Put(l)
|
if p.peek(itemObjClose) {
|
||||||
|
p.ignore()
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, err
|
return nil, fmt.Errorf("operation missing closing '}'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !p.peek(itemEOF) {
|
||||||
|
p.ignore()
|
||||||
|
return nil, fmt.Errorf("invalid '%s' found after closing '}'", p.current())
|
||||||
|
}
|
||||||
|
|
||||||
|
lexPool.Put(l)
|
||||||
|
|
||||||
return op, err
|
return op, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,11 +191,16 @@ func (p *Parser) ignore() {
|
|||||||
p.pos = n
|
p.pos = n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Parser) current() string {
|
||||||
|
item := p.items[p.pos]
|
||||||
|
return b2s(p.input[item.pos:item.end])
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Parser) peek(types ...itemType) bool {
|
func (p *Parser) peek(types ...itemType) bool {
|
||||||
n := p.pos + 1
|
n := p.pos + 1
|
||||||
if p.items[n]._type == itemEOF {
|
// if p.items[n]._type == itemEOF {
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
if n >= len(p.items) {
|
if n >= len(p.items) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -292,9 +304,10 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
|
|||||||
|
|
||||||
if st.Len() == 0 {
|
if st.Len() == 0 {
|
||||||
break
|
break
|
||||||
}
|
} else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !p.peek(itemName) {
|
if !p.peek(itemName) {
|
||||||
return nil, errors.New("expecting an alias or field name")
|
return nil, errors.New("expecting an alias or field name")
|
||||||
@ -306,6 +319,8 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
|
|||||||
f.Args = f.argsA[:0]
|
f.Args = f.argsA[:0]
|
||||||
f.Children = f.childrenA[:0]
|
f.Children = f.childrenA[:0]
|
||||||
|
|
||||||
|
// Parse the inside of the the fields () parentheses
|
||||||
|
// in short parse the args like id, where, etc
|
||||||
if err := p.parseField(f); err != nil {
|
if err := p.parseField(f); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -318,6 +333,8 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
|
|||||||
f.ParentID = -1
|
f.ParentID = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The first opening curley brackets after this
|
||||||
|
// comes the columns or child fields
|
||||||
if p.peek(itemObjOpen) {
|
if p.peek(itemObjOpen) {
|
||||||
p.ignore()
|
p.ignore()
|
||||||
st.Push(f.ID)
|
st.Push(f.ID)
|
||||||
|
@ -17,7 +17,7 @@ func TestCompile1(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = qc.Compile([]byte(`
|
_, err = qc.Compile([]byte(`
|
||||||
{ product(id: 15) {
|
query { product(id: 15) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
} }`), "user")
|
} }`), "user")
|
||||||
@ -100,6 +100,35 @@ func TestEmptyCompile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInvalidPostfixCompile(t *testing.T) {
|
||||||
|
gql := `mutation
|
||||||
|
updateThread {
|
||||||
|
thread(update: $data, where: { slug: { eq: $slug } }) {
|
||||||
|
slug
|
||||||
|
title
|
||||||
|
published
|
||||||
|
createdAt : created_at
|
||||||
|
totalVotes : cached_votes_total
|
||||||
|
totalPosts : cached_posts_total
|
||||||
|
vote : thread_vote(where: { user_id: { eq: $user_id } }) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
topics {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
qcompile, _ := NewCompiler(Config{})
|
||||||
|
_, err := qcompile.Compile([]byte(gql), "anon")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(errors.New("expecting an error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var gql = []byte(`
|
var gql = []byte(`
|
||||||
products(
|
products(
|
||||||
# returns only 30 items
|
# returns only 30 items
|
||||||
|
@ -51,6 +51,7 @@ type Select struct {
|
|||||||
Allowed map[string]struct{}
|
Allowed map[string]struct{}
|
||||||
PresetMap map[string]string
|
PresetMap map[string]string
|
||||||
PresetList []string
|
PresetList []string
|
||||||
|
SkipRender bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Column struct {
|
type Column struct {
|
||||||
@ -187,7 +188,7 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
|||||||
trv := &trval{}
|
trv := &trval{}
|
||||||
|
|
||||||
// query config
|
// query config
|
||||||
trv.query.fil, err = compileFilter(trc.Query.Filters)
|
trv.query.fil, trv.query.filNU, err = compileFilter(trc.Query.Filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -198,7 +199,8 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
|||||||
trv.query.disable.funcs = trc.Query.DisableFunctions
|
trv.query.disable.funcs = trc.Query.DisableFunctions
|
||||||
|
|
||||||
// insert config
|
// insert config
|
||||||
if trv.insert.fil, err = compileFilter(trc.Insert.Filters); err != nil {
|
trv.insert.fil, trv.insert.filNU, err = compileFilter(trc.Insert.Filters)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
trv.insert.cols = listToMap(trc.Insert.Columns)
|
trv.insert.cols = listToMap(trc.Insert.Columns)
|
||||||
@ -206,7 +208,8 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
|||||||
trv.insert.pslist = mapToList(trv.insert.psmap)
|
trv.insert.pslist = mapToList(trv.insert.psmap)
|
||||||
|
|
||||||
// update config
|
// update config
|
||||||
if trv.update.fil, err = compileFilter(trc.Update.Filters); err != nil {
|
trv.update.fil, trv.update.filNU, err = compileFilter(trc.Update.Filters)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
trv.update.cols = listToMap(trc.Update.Columns)
|
trv.update.cols = listToMap(trc.Update.Columns)
|
||||||
@ -214,7 +217,8 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
|||||||
trv.update.pslist = mapToList(trv.update.psmap)
|
trv.update.pslist = mapToList(trv.update.psmap)
|
||||||
|
|
||||||
// delete config
|
// delete config
|
||||||
if trv.delete.fil, err = compileFilter(trc.Delete.Filters); err != nil {
|
trv.delete.fil, trv.delete.filNU, err = compileFilter(trc.Delete.Filters)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
trv.delete.cols = listToMap(trc.Delete.Columns)
|
trv.delete.cols = listToMap(trc.Delete.Columns)
|
||||||
@ -334,7 +338,7 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
|||||||
s.FieldName = s.Name
|
s.FieldName = s.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
err := com.compileArgs(qc, s, field.Args)
|
err := com.compileArgs(qc, s, field.Args, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -388,9 +392,16 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
|||||||
|
|
||||||
func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
||||||
var fil *Exp
|
var fil *Exp
|
||||||
|
var nu bool
|
||||||
|
|
||||||
if trv, ok := com.tr[role][sel.Name]; ok {
|
if trv, ok := com.tr[role][sel.Name]; ok {
|
||||||
fil = trv.filter(qc.Type)
|
fil, nu = trv.filter(qc.Type)
|
||||||
|
|
||||||
|
} else if role == "anon" {
|
||||||
|
// Tables not defined under the anon role will not be rendered
|
||||||
|
sel.SkipRender = true
|
||||||
|
return
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -399,6 +410,10 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nu && role == "anon" {
|
||||||
|
sel.SkipRender = true
|
||||||
|
}
|
||||||
|
|
||||||
switch fil.Op {
|
switch fil.Op {
|
||||||
case OpNop:
|
case OpNop:
|
||||||
case OpFalse:
|
case OpFalse:
|
||||||
@ -420,7 +435,7 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg) error {
|
func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg, role string) error {
|
||||||
var err error
|
var err error
|
||||||
var ka bool
|
var ka bool
|
||||||
|
|
||||||
@ -435,7 +450,7 @@ func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg) error {
|
|||||||
err, ka = com.compileArgSearch(sel, arg)
|
err, ka = com.compileArgSearch(sel, arg)
|
||||||
|
|
||||||
case "where":
|
case "where":
|
||||||
err, ka = com.compileArgWhere(sel, arg)
|
err, ka = com.compileArgWhere(sel, arg, role)
|
||||||
|
|
||||||
case "orderby", "order_by", "order":
|
case "orderby", "order_by", "order":
|
||||||
err, ka = com.compileArgOrderBy(sel, arg)
|
err, ka = com.compileArgOrderBy(sel, arg)
|
||||||
@ -501,19 +516,20 @@ func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (com *Compiler) compileArgObj(st *util.Stack, arg *Arg) (*Exp, error) {
|
func (com *Compiler) compileArgObj(st *util.Stack, arg *Arg) (*Exp, bool, error) {
|
||||||
if arg.Val.Type != NodeObj {
|
if arg.Val.Type != NodeObj {
|
||||||
return nil, fmt.Errorf("expecting an object")
|
return nil, false, fmt.Errorf("expecting an object")
|
||||||
}
|
}
|
||||||
|
|
||||||
return com.compileArgNode(st, arg.Val, true)
|
return com.compileArgNode(st, arg.Val, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*Exp, bool, error) {
|
||||||
var root *Exp
|
var root *Exp
|
||||||
|
var needsUser bool
|
||||||
|
|
||||||
if node == nil || len(node.Children) == 0 {
|
if node == nil || len(node.Children) == 0 {
|
||||||
return nil, errors.New("invalid argument value")
|
return nil, needsUser, errors.New("invalid argument value")
|
||||||
}
|
}
|
||||||
|
|
||||||
pushChild(st, nil, node)
|
pushChild(st, nil, node)
|
||||||
@ -526,7 +542,7 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
|||||||
intf := st.Pop()
|
intf := st.Pop()
|
||||||
node, ok := intf.(*Node)
|
node, ok := intf.(*Node)
|
||||||
if !ok || node == nil {
|
if !ok || node == nil {
|
||||||
return nil, fmt.Errorf("16: unexpected value %v (%t)", intf, intf)
|
return nil, needsUser, fmt.Errorf("16: unexpected value %v (%t)", intf, intf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Objects inside a list
|
// Objects inside a list
|
||||||
@ -542,13 +558,17 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
|||||||
|
|
||||||
ex, err := newExp(st, node, usePool)
|
ex, err := newExp(st, node, usePool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, needsUser, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ex == nil {
|
if ex == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ex.Type == ValVar && ex.Val == "user_id" {
|
||||||
|
needsUser = true
|
||||||
|
}
|
||||||
|
|
||||||
if node.exp == nil {
|
if node.exp == nil {
|
||||||
root = ex
|
root = ex
|
||||||
} else {
|
} else {
|
||||||
@ -571,7 +591,7 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
|||||||
nodePool.Put(node)
|
nodePool.Put(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
return root, nil
|
return root, needsUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) {
|
func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) {
|
||||||
@ -640,15 +660,19 @@ func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) {
|
|||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg) (error, bool) {
|
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) (error, bool) {
|
||||||
st := util.NewStack()
|
st := util.NewStack()
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
ex, err := com.compileArgObj(st, arg)
|
ex, nu, err := com.compileArgObj(st, arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, false
|
return err, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nu && role == "anon" {
|
||||||
|
sel.SkipRender = true
|
||||||
|
}
|
||||||
|
|
||||||
if sel.Where != nil {
|
if sel.Where != nil {
|
||||||
ow := sel.Where
|
ow := sel.Where
|
||||||
|
|
||||||
@ -976,27 +1000,32 @@ func pushChild(st *util.Stack, exp *Exp, node *Node) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileFilter(filter []string) (*Exp, error) {
|
func compileFilter(filter []string) (*Exp, bool, error) {
|
||||||
var fl *Exp
|
var fl *Exp
|
||||||
|
var needsUser bool
|
||||||
|
|
||||||
com := &Compiler{}
|
com := &Compiler{}
|
||||||
st := util.NewStack()
|
st := util.NewStack()
|
||||||
|
|
||||||
if len(filter) == 0 {
|
if len(filter) == 0 {
|
||||||
return &Exp{Op: OpNop, doFree: false}, nil
|
return &Exp{Op: OpNop, doFree: false}, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range filter {
|
for i := range filter {
|
||||||
if filter[i] == "false" {
|
if filter[i] == "false" {
|
||||||
return &Exp{Op: OpFalse, doFree: false}, nil
|
return &Exp{Op: OpFalse, doFree: false}, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
node, err := ParseArgValue(filter[i])
|
node, err := ParseArgValue(filter[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
f, err := com.compileArgNode(st, node, false)
|
f, nu, err := com.compileArgNode(st, node, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if nu {
|
||||||
|
needsUser = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Invalid table names in nested where causes fail silently
|
// TODO: Invalid table names in nested where causes fail silently
|
||||||
@ -1010,7 +1039,7 @@ func compileFilter(filter []string) (*Exp, error) {
|
|||||||
fl = &Exp{Op: OpAnd, Children: []*Exp{fl, f}, doFree: false}
|
fl = &Exp{Op: OpAnd, Children: []*Exp{fl, f}, doFree: false}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fl, nil
|
return fl, needsUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPath(a []string) string {
|
func buildPath(a []string) string {
|
||||||
|
41
serv/actions.go
Normal file
41
serv/actions.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package serv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type actionFn func(w http.ResponseWriter, r *http.Request) error
|
||||||
|
|
||||||
|
func newAction(a configAction) (http.Handler, error) {
|
||||||
|
var fn actionFn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(a.SQL) != 0 {
|
||||||
|
fn, err = newSQLAction(a)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid config for action '%s'", a.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpFn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := fn(w, r); err != nil {
|
||||||
|
errlog.Error().Err(err).Send()
|
||||||
|
errorResp(w, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(httpFn), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSQLAction(a configAction) (actionFn, error) {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
_, err := db.Exec(r.Context(), a.SQL)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn, nil
|
||||||
|
}
|
320
serv/allow.go
320
serv/allow.go
@ -1,320 +0,0 @@
|
|||||||
package serv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
AL_QUERY int = iota + 1
|
|
||||||
AL_VARS
|
|
||||||
)
|
|
||||||
|
|
||||||
type allowItem struct {
|
|
||||||
name string
|
|
||||||
hash string
|
|
||||||
uri string
|
|
||||||
gql string
|
|
||||||
vars json.RawMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
var _allowList allowList
|
|
||||||
|
|
||||||
type allowList struct {
|
|
||||||
list []*allowItem
|
|
||||||
index map[string]int
|
|
||||||
filepath string
|
|
||||||
saveChan chan *allowItem
|
|
||||||
active bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func initAllowList(cpath string) {
|
|
||||||
_allowList = allowList{
|
|
||||||
index: make(map[string]int),
|
|
||||||
saveChan: make(chan *allowItem),
|
|
||||||
active: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cpath) != 0 {
|
|
||||||
fp := path.Join(cpath, "allow.list")
|
|
||||||
|
|
||||||
if _, err := os.Stat(fp); err == nil {
|
|
||||||
_allowList.filepath = fp
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
errlog.Fatal().Err(err).Send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(_allowList.filepath) == 0 {
|
|
||||||
fp := "./allow.list"
|
|
||||||
|
|
||||||
if _, err := os.Stat(fp); err == nil {
|
|
||||||
_allowList.filepath = fp
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
errlog.Fatal().Err(err).Send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(_allowList.filepath) == 0 {
|
|
||||||
fp := "./config/allow.list"
|
|
||||||
|
|
||||||
if _, err := os.Stat(fp); err == nil {
|
|
||||||
_allowList.filepath = fp
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
errlog.Fatal().Err(err).Send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(_allowList.filepath) == 0 {
|
|
||||||
if conf.Production {
|
|
||||||
errlog.Fatal().Msg("allow.list not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cpath) == 0 {
|
|
||||||
_allowList.filepath = "./config/allow.list"
|
|
||||||
} else {
|
|
||||||
_allowList.filepath = path.Join(cpath, "allow.list")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Warn().Msg("allow.list not found")
|
|
||||||
} else {
|
|
||||||
_allowList.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for v := range _allowList.saveChan {
|
|
||||||
_allowList.save(v)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (al *allowList) add(req *gqlReq) {
|
|
||||||
if al.saveChan == nil || len(req.ref) == 0 || len(req.Query) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var query string
|
|
||||||
|
|
||||||
for i := 0; i < len(req.Query); i++ {
|
|
||||||
c := req.Query[i]
|
|
||||||
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' {
|
|
||||||
query = req.Query
|
|
||||||
break
|
|
||||||
|
|
||||||
} else if c == '{' {
|
|
||||||
query = "query " + req.Query
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
al.saveChan <- &allowItem{
|
|
||||||
uri: req.ref,
|
|
||||||
gql: query,
|
|
||||||
vars: req.Vars,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (al *allowList) upsert(query, vars []byte, uri string) {
|
|
||||||
q := string(query)
|
|
||||||
hash := gqlHash(q, vars, "")
|
|
||||||
name := gqlName(q)
|
|
||||||
|
|
||||||
var key string
|
|
||||||
|
|
||||||
if len(name) == 0 {
|
|
||||||
key = hash
|
|
||||||
} else {
|
|
||||||
key = name
|
|
||||||
}
|
|
||||||
|
|
||||||
if i, ok := al.index[key]; !ok {
|
|
||||||
al.list = append(al.list, &allowItem{
|
|
||||||
name: name,
|
|
||||||
hash: hash,
|
|
||||||
uri: uri,
|
|
||||||
gql: q,
|
|
||||||
vars: vars,
|
|
||||||
})
|
|
||||||
al.index[key] = len(al.list) - 1
|
|
||||||
} else {
|
|
||||||
item := al.list[i]
|
|
||||||
item.name = name
|
|
||||||
item.hash = hash
|
|
||||||
item.gql = q
|
|
||||||
item.vars = vars
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (al *allowList) load() {
|
|
||||||
b, err := ioutil.ReadFile(al.filepath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(b) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var uri string
|
|
||||||
var varBytes []byte
|
|
||||||
|
|
||||||
s, e, c := 0, 0, 0
|
|
||||||
ty := 0
|
|
||||||
|
|
||||||
for {
|
|
||||||
if c == 0 && b[e] == '#' {
|
|
||||||
s = e
|
|
||||||
for e < len(b) && b[e] != '\n' {
|
|
||||||
e++
|
|
||||||
}
|
|
||||||
if (e - s) > 2 {
|
|
||||||
uri = strings.TrimSpace(string(b[(s + 1):e]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if e >= len(b) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if matchPrefix(b, e, "query") || matchPrefix(b, e, "mutation") {
|
|
||||||
if c == 0 {
|
|
||||||
s = e
|
|
||||||
}
|
|
||||||
ty = AL_QUERY
|
|
||||||
} else if matchPrefix(b, e, "variables") {
|
|
||||||
if c == 0 {
|
|
||||||
s = e + len("variables") + 1
|
|
||||||
}
|
|
||||||
ty = AL_VARS
|
|
||||||
} else if b[e] == '{' {
|
|
||||||
c++
|
|
||||||
|
|
||||||
} else if b[e] == '}' {
|
|
||||||
c--
|
|
||||||
|
|
||||||
if c == 0 {
|
|
||||||
if ty == AL_QUERY {
|
|
||||||
al.upsert(b[s:(e+1)], varBytes, uri)
|
|
||||||
varBytes = nil
|
|
||||||
|
|
||||||
} else if ty == AL_VARS {
|
|
||||||
varBytes = b[s:(e + 1)]
|
|
||||||
}
|
|
||||||
ty = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
e++
|
|
||||||
if e >= len(b) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (al *allowList) save(item *allowItem) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
item.hash = gqlHash(item.gql, item.vars, "")
|
|
||||||
item.name = gqlName(item.gql)
|
|
||||||
|
|
||||||
if len(item.name) == 0 {
|
|
||||||
key := item.hash
|
|
||||||
|
|
||||||
if _, ok := al.index[key]; ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
al.list = append(al.list, item)
|
|
||||||
al.index[key] = len(al.list) - 1
|
|
||||||
|
|
||||||
} else {
|
|
||||||
key := item.name
|
|
||||||
|
|
||||||
if i, ok := al.index[key]; ok {
|
|
||||||
if al.list[i].hash == item.hash {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
al.list[i] = item
|
|
||||||
} else {
|
|
||||||
al.list = append(al.list, item)
|
|
||||||
al.index[key] = len(al.list) - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(al.filepath)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msgf("Failed to write allow list: %s", al.filepath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
keys := []string{}
|
|
||||||
urlMap := make(map[string][]*allowItem)
|
|
||||||
|
|
||||||
for _, v := range al.list {
|
|
||||||
urlMap[v.uri] = append(urlMap[v.uri], v)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k := range urlMap {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
for i := range keys {
|
|
||||||
k := keys[i]
|
|
||||||
v := urlMap[k]
|
|
||||||
|
|
||||||
if _, err := f.WriteString(fmt.Sprintf("# %s\n\n", k)); err != nil {
|
|
||||||
logger.Error().Err(err).Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range v {
|
|
||||||
if len(v[i].vars) != 0 && !bytes.Equal(v[i].vars, []byte("{}")) {
|
|
||||||
vj, err := json.MarshalIndent(v[i].vars, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("Failed to write allow list 'vars' to file")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v[i].gql[0] == '{' {
|
|
||||||
_, err = f.WriteString(fmt.Sprintf("query %s\n\n", v[i].gql))
|
|
||||||
} else {
|
|
||||||
_, err = f.WriteString(fmt.Sprintf("%s\n\n", v[i].gql))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchPrefix(b []byte, i int, s string) bool {
|
|
||||||
if (len(b) - i) < len(s) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for n := 0; n < len(s); n++ {
|
|
||||||
if b[(i+n)] != s[n] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
33
serv/args.go
33
serv/args.go
@ -34,6 +34,7 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
|
|||||||
}
|
}
|
||||||
|
|
||||||
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
|
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
|
||||||
|
|
||||||
if len(fields) == 0 {
|
if len(fields) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
@ -42,7 +43,7 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
|
|||||||
fields[0].Value = v[1 : len(v)-1]
|
fields[0].Value = v[1 : len(v)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Write(fields[0].Value)
|
return w.Write(escQuote(fields[0].Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
|||||||
if v, ok := fields[string(av)]; ok {
|
if v, ok := fields[string(av)]; ok {
|
||||||
switch v[0] {
|
switch v[0] {
|
||||||
case '[', '{':
|
case '[', '{':
|
||||||
vars[i] = v
|
vars[i] = escQuote(v)
|
||||||
default:
|
default:
|
||||||
var val interface{}
|
var val interface{}
|
||||||
if err := json.Unmarshal(v, &val); err != nil {
|
if err := json.Unmarshal(v, &val); err != nil {
|
||||||
@ -106,3 +107,31 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
|||||||
|
|
||||||
return vars, nil
|
return vars, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func escQuote(b []byte) []byte {
|
||||||
|
f := false
|
||||||
|
for i := range b {
|
||||||
|
if b[i] == '\'' {
|
||||||
|
f = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !f {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
s := 0
|
||||||
|
for i := range b {
|
||||||
|
if b[i] == '\'' {
|
||||||
|
buf.Write(b[s:i])
|
||||||
|
buf.WriteString(`''`)
|
||||||
|
s = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l := len(b)
|
||||||
|
if s < (l - 1) {
|
||||||
|
buf.Write(b[s:l])
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
60
serv/auth.go
60
serv/auth.go
@ -3,7 +3,6 @@ package serv
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ctxkey int
|
type ctxkey int
|
||||||
@ -14,7 +13,7 @@ const (
|
|||||||
userRoleKey
|
userRoleKey
|
||||||
)
|
)
|
||||||
|
|
||||||
func headerAuth(next http.Handler) http.HandlerFunc {
|
func headerAuth(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
@ -37,28 +36,53 @@ func headerAuth(next http.Handler) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withAuth(next http.Handler) http.Handler {
|
func headerHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||||
at := conf.Auth.Type
|
hdr := authc.Header
|
||||||
ru := conf.Auth.Rails.URL
|
|
||||||
|
|
||||||
if conf.Auth.CredsInHeader {
|
if len(hdr.Name) == 0 {
|
||||||
next = headerAuth(next)
|
errlog.Fatal().Str("auth", authc.Name).Msg("no header.name defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch at {
|
if !hdr.Exists && len(hdr.Value) == 0 {
|
||||||
|
errlog.Fatal().Str("auth", authc.Name).Msg("no header.value defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var fo1 bool
|
||||||
|
value := r.Header.Get(hdr.Name)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case hdr.Exists:
|
||||||
|
fo1 = (len(value) == 0)
|
||||||
|
|
||||||
|
default:
|
||||||
|
fo1 = (value != hdr.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fo1 {
|
||||||
|
http.Error(w, "401 unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withAuth(next http.Handler, authc configAuth) http.Handler {
|
||||||
|
if authc.CredsInHeader {
|
||||||
|
next = headerAuth(authc, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch authc.Type {
|
||||||
case "rails":
|
case "rails":
|
||||||
if strings.HasPrefix(ru, "memcache:") {
|
return railsHandler(authc, next)
|
||||||
return railsMemcacheHandler(next)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(ru, "redis:") {
|
|
||||||
return railsRedisHandler(next)
|
|
||||||
}
|
|
||||||
|
|
||||||
return railsCookieHandler(next)
|
|
||||||
|
|
||||||
case "jwt":
|
case "jwt":
|
||||||
return jwtHandler(next)
|
return jwtHandler(authc, next)
|
||||||
|
|
||||||
|
case "header":
|
||||||
|
return headerHandler(authc, next)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return next
|
return next
|
||||||
|
@ -14,18 +14,18 @@ const (
|
|||||||
jwtAuth0 int = iota + 1
|
jwtAuth0 int = iota + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
func jwtHandler(next http.Handler) http.HandlerFunc {
|
func jwtHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||||
var key interface{}
|
var key interface{}
|
||||||
var jwtProvider int
|
var jwtProvider int
|
||||||
|
|
||||||
cookie := conf.Auth.Cookie
|
cookie := authc.Cookie
|
||||||
|
|
||||||
if conf.Auth.JWT.Provider == "auth0" {
|
if authc.JWT.Provider == "auth0" {
|
||||||
jwtProvider = jwtAuth0
|
jwtProvider = jwtAuth0
|
||||||
}
|
}
|
||||||
|
|
||||||
secret := conf.Auth.JWT.Secret
|
secret := authc.JWT.Secret
|
||||||
publicKeyFile := conf.Auth.JWT.PubKeyFile
|
publicKeyFile := authc.JWT.PubKeyFile
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(secret) != 0:
|
case len(secret) != 0:
|
||||||
@ -37,7 +37,7 @@ func jwtHandler(next http.Handler) http.HandlerFunc {
|
|||||||
errlog.Fatal().Err(err).Send()
|
errlog.Fatal().Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
switch conf.Auth.JWT.PubKeyType {
|
switch authc.JWT.PubKeyType {
|
||||||
case "ecdsa":
|
case "ecdsa":
|
||||||
key, err = jwt.ParseECPublicKeyFromPEM(kd)
|
key, err = jwt.ParseECPublicKeyFromPEM(kd)
|
||||||
|
|
||||||
|
@ -6,32 +6,47 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/bradfitz/gomemcache/memcache"
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
"github.com/dosco/super-graph/rails"
|
"github.com/dosco/super-graph/rails"
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/garyburd/redigo/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
func railsRedisHandler(next http.Handler) http.HandlerFunc {
|
func railsHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||||
cookie := conf.Auth.Cookie
|
ru := authc.Rails.URL
|
||||||
|
|
||||||
|
if strings.HasPrefix(ru, "memcache:") {
|
||||||
|
return railsMemcacheHandler(authc, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(ru, "redis:") {
|
||||||
|
return railsRedisHandler(authc, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return railsCookieHandler(authc, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
func railsRedisHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||||
|
cookie := authc.Cookie
|
||||||
if len(cookie) == 0 {
|
if len(cookie) == 0 {
|
||||||
errlog.Fatal().Msg("no auth.cookie defined")
|
errlog.Fatal().Msg("no auth.cookie defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(conf.Auth.Rails.URL) == 0 {
|
if len(authc.Rails.URL) == 0 {
|
||||||
errlog.Fatal().Msg("no auth.rails.url defined")
|
errlog.Fatal().Msg("no auth.rails.url defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
rp := &redis.Pool{
|
rp := &redis.Pool{
|
||||||
MaxIdle: conf.Auth.Rails.MaxIdle,
|
MaxIdle: authc.Rails.MaxIdle,
|
||||||
MaxActive: conf.Auth.Rails.MaxActive,
|
MaxActive: authc.Rails.MaxActive,
|
||||||
Dial: func() (redis.Conn, error) {
|
Dial: func() (redis.Conn, error) {
|
||||||
c, err := redis.DialURL(conf.Auth.Rails.URL)
|
c, err := redis.DialURL(authc.Rails.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errlog.Fatal().Err(err).Send()
|
errlog.Fatal().Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
pwd := conf.Auth.Rails.Password
|
pwd := authc.Rails.Password
|
||||||
if len(pwd) != 0 {
|
if len(pwd) != 0 {
|
||||||
if _, err := c.Do("AUTH", pwd); err != nil {
|
if _, err := c.Do("AUTH", pwd); err != nil {
|
||||||
errlog.Fatal().Err(err).Send()
|
errlog.Fatal().Err(err).Send()
|
||||||
@ -66,17 +81,17 @@ func railsRedisHandler(next http.Handler) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func railsMemcacheHandler(next http.Handler) http.HandlerFunc {
|
func railsMemcacheHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||||
cookie := conf.Auth.Cookie
|
cookie := authc.Cookie
|
||||||
if len(cookie) == 0 {
|
if len(cookie) == 0 {
|
||||||
errlog.Fatal().Msg("no auth.cookie defined")
|
errlog.Fatal().Msg("no auth.cookie defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(conf.Auth.Rails.URL) == 0 {
|
if len(authc.Rails.URL) == 0 {
|
||||||
errlog.Fatal().Msg("no auth.rails.url defined")
|
errlog.Fatal().Msg("no auth.rails.url defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
rURL, err := url.Parse(conf.Auth.Rails.URL)
|
rURL, err := url.Parse(authc.Rails.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errlog.Fatal().Err(err).Send()
|
errlog.Fatal().Err(err).Send()
|
||||||
}
|
}
|
||||||
@ -108,13 +123,13 @@ func railsMemcacheHandler(next http.Handler) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func railsCookieHandler(next http.Handler) http.HandlerFunc {
|
func railsCookieHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||||
cookie := conf.Auth.Cookie
|
cookie := authc.Cookie
|
||||||
if len(cookie) == 0 {
|
if len(cookie) == 0 {
|
||||||
errlog.Fatal().Msg("no auth.cookie defined")
|
errlog.Fatal().Msg("no auth.cookie defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
ra, err := railsAuth(conf)
|
ra, err := railsAuth(authc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errlog.Fatal().Err(err).Send()
|
errlog.Fatal().Err(err).Send()
|
||||||
}
|
}
|
||||||
@ -139,13 +154,13 @@ func railsCookieHandler(next http.Handler) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func railsAuth(c *config) (*rails.Auth, error) {
|
func railsAuth(authc configAuth) (*rails.Auth, error) {
|
||||||
secret := c.Auth.Rails.SecretKeyBase
|
secret := authc.Rails.SecretKeyBase
|
||||||
if len(secret) == 0 {
|
if len(secret) == 0 {
|
||||||
return nil, errors.New("no auth.rails.secret_key_base defined")
|
return nil, errors.New("no auth.rails.secret_key_base defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
version := c.Auth.Rails.Version
|
version := authc.Rails.Version
|
||||||
if len(version) == 0 {
|
if len(version) == 0 {
|
||||||
return nil, errors.New("no auth.rails.version defined")
|
return nil, errors.New("no auth.rails.version defined")
|
||||||
}
|
}
|
||||||
@ -155,16 +170,16 @@ func railsAuth(c *config) (*rails.Auth, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.Auth.Rails.Salt) != 0 {
|
if len(authc.Rails.Salt) != 0 {
|
||||||
ra.Salt = c.Auth.Rails.Salt
|
ra.Salt = authc.Rails.Salt
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(conf.Auth.Rails.SignSalt) != 0 {
|
if len(authc.Rails.SignSalt) != 0 {
|
||||||
ra.SignSalt = c.Auth.Rails.SignSalt
|
ra.SignSalt = authc.Rails.SignSalt
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(conf.Auth.Rails.AuthSalt) != 0 {
|
if len(authc.Rails.AuthSalt) != 0 {
|
||||||
ra.AuthSalt = c.Auth.Rails.AuthSalt
|
ra.AuthSalt = authc.Rails.AuthSalt
|
||||||
}
|
}
|
||||||
|
|
||||||
return ra, nil
|
return ra, nil
|
||||||
|
162
serv/cmd.go
162
serv/cmd.go
@ -1,15 +1,13 @@
|
|||||||
package serv
|
package serv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dosco/super-graph/allow"
|
||||||
"github.com/dosco/super-graph/psql"
|
"github.com/dosco/super-graph/psql"
|
||||||
"github.com/dosco/super-graph/qcode"
|
"github.com/dosco/super-graph/qcode"
|
||||||
"github.com/jackc/pgx/v4"
|
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -37,11 +35,12 @@ var (
|
|||||||
confPath string // path to the config file
|
confPath string // path to the config file
|
||||||
db *pgxpool.Pool // database connection pool
|
db *pgxpool.Pool // database connection pool
|
||||||
schema *psql.DBSchema // database tables, columns and relationships
|
schema *psql.DBSchema // database tables, columns and relationships
|
||||||
|
allowList *allow.List // allow.list is contains queries allowed in production
|
||||||
qcompile *qcode.Compiler // qcode compiler
|
qcompile *qcode.Compiler // qcode compiler
|
||||||
pcompile *psql.Compiler // postgres sql compiler
|
pcompile *psql.Compiler // postgres sql compiler
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Cmd() {
|
||||||
initLog()
|
initLog()
|
||||||
|
|
||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
@ -156,159 +155,6 @@ e.g. db:migrate -+1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initLog() {
|
|
||||||
out := zerolog.ConsoleWriter{Out: os.Stderr}
|
|
||||||
logger = zerolog.New(out).With().Timestamp().Logger()
|
|
||||||
errlog = logger.With().Caller().Logger()
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConf() (*config, error) {
|
|
||||||
vi := newConfig(getConfigName())
|
|
||||||
|
|
||||||
if err := vi.ReadInConfig(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
inherits := vi.GetString("inherits")
|
|
||||||
if len(inherits) != 0 {
|
|
||||||
vi = newConfig(inherits)
|
|
||||||
|
|
||||||
if err := vi.ReadInConfig(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if vi.IsSet("inherits") {
|
|
||||||
errlog.Fatal().Msgf("inherited config (%s) cannot itself inherit (%s)",
|
|
||||||
inherits,
|
|
||||||
vi.GetString("inherits"))
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.SetConfigName(getConfigName())
|
|
||||||
|
|
||||||
if err := vi.MergeInConfig(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &config{}
|
|
||||||
|
|
||||||
if err := c.Init(vi); err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to decode config, %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logLevel, err := zerolog.ParseLevel(c.LogLevel)
|
|
||||||
if err != nil {
|
|
||||||
errlog.Error().Err(err).Msg("error setting log_level")
|
|
||||||
}
|
|
||||||
zerolog.SetGlobalLevel(logLevel)
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDB(c *config, useDB bool) (*pgx.Conn, error) {
|
|
||||||
config, _ := pgx.ParseConfig("")
|
|
||||||
config.Host = c.DB.Host
|
|
||||||
config.Port = c.DB.Port
|
|
||||||
config.User = c.DB.User
|
|
||||||
config.Password = c.DB.Password
|
|
||||||
config.RuntimeParams = map[string]string{
|
|
||||||
"application_name": c.AppName,
|
|
||||||
"search_path": c.DB.Schema,
|
|
||||||
}
|
|
||||||
|
|
||||||
if useDB {
|
|
||||||
config.Database = c.DB.DBName
|
|
||||||
}
|
|
||||||
|
|
||||||
switch c.LogLevel {
|
|
||||||
case "debug":
|
|
||||||
config.LogLevel = pgx.LogLevelDebug
|
|
||||||
case "info":
|
|
||||||
config.LogLevel = pgx.LogLevelInfo
|
|
||||||
case "warn":
|
|
||||||
config.LogLevel = pgx.LogLevelWarn
|
|
||||||
case "error":
|
|
||||||
config.LogLevel = pgx.LogLevelError
|
|
||||||
default:
|
|
||||||
config.LogLevel = pgx.LogLevelNone
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Logger = NewSQLLogger(logger)
|
|
||||||
|
|
||||||
db, err := pgx.ConnectConfig(context.Background(), config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDBPool(c *config) (*pgxpool.Pool, error) {
|
|
||||||
config, _ := pgxpool.ParseConfig("")
|
|
||||||
config.ConnConfig.Host = c.DB.Host
|
|
||||||
config.ConnConfig.Port = c.DB.Port
|
|
||||||
config.ConnConfig.Database = c.DB.DBName
|
|
||||||
config.ConnConfig.User = c.DB.User
|
|
||||||
config.ConnConfig.Password = c.DB.Password
|
|
||||||
config.ConnConfig.RuntimeParams = map[string]string{
|
|
||||||
"application_name": c.AppName,
|
|
||||||
"search_path": c.DB.Schema,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch c.LogLevel {
|
|
||||||
case "debug":
|
|
||||||
config.ConnConfig.LogLevel = pgx.LogLevelDebug
|
|
||||||
case "info":
|
|
||||||
config.ConnConfig.LogLevel = pgx.LogLevelInfo
|
|
||||||
case "warn":
|
|
||||||
config.ConnConfig.LogLevel = pgx.LogLevelWarn
|
|
||||||
case "error":
|
|
||||||
config.ConnConfig.LogLevel = pgx.LogLevelError
|
|
||||||
default:
|
|
||||||
config.ConnConfig.LogLevel = pgx.LogLevelNone
|
|
||||||
}
|
|
||||||
|
|
||||||
config.ConnConfig.Logger = NewSQLLogger(logger)
|
|
||||||
|
|
||||||
// if c.DB.MaxRetries != 0 {
|
|
||||||
// opt.MaxRetries = c.DB.MaxRetries
|
|
||||||
// }
|
|
||||||
|
|
||||||
if c.DB.PoolSize != 0 {
|
|
||||||
config.MaxConns = conf.DB.PoolSize
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := pgxpool.ConnectConfig(context.Background(), config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initCompiler() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
qcompile, pcompile, err = initCompilers(conf)
|
|
||||||
if err != nil {
|
|
||||||
errlog.Fatal().Err(err).Msg("failed to initialize compilers")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := initResolvers(); err != nil {
|
|
||||||
errlog.Fatal().Err(err).Msg("failed to initialized resolvers")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConfOnce() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if conf == nil {
|
|
||||||
if conf, err = initConf(); err != nil {
|
|
||||||
errlog.Fatal().Err(err).Msg("failed to read config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cmdVersion(cmd *cobra.Command, args []string) {
|
func cmdVersion(cmd *cobra.Command, args []string) {
|
||||||
fmt.Printf("%s\n", BuildDetails())
|
fmt.Printf("%s\n", BuildDetails())
|
||||||
}
|
}
|
||||||
@ -324,7 +170,7 @@ Branch : %v
|
|||||||
Go version : %v
|
Go version : %v
|
||||||
|
|
||||||
Licensed under the Apache Public License 2.0
|
Licensed under the Apache Public License 2.0
|
||||||
Copyright 2015-2019 Vikram Rangnekar.
|
Copyright 2020, Vikram Rangnekar.
|
||||||
`,
|
`,
|
||||||
version,
|
version,
|
||||||
lastCommitSHA,
|
lastCommitSHA,
|
||||||
|
@ -63,7 +63,7 @@ func cmdDBCreate(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
defer conn.Close(ctx)
|
defer conn.Close(ctx)
|
||||||
|
|
||||||
sql := fmt.Sprintf("CREATE DATABASE %s", conf.DB.DBName)
|
sql := fmt.Sprintf(`CREATE DATABASE "%s"`, conf.DB.DBName)
|
||||||
|
|
||||||
_, err = conn.Exec(ctx, sql)
|
_, err = conn.Exec(ctx, sql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -83,7 +83,7 @@ func cmdDBDrop(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
defer conn.Close(ctx)
|
defer conn.Close(ctx)
|
||||||
|
|
||||||
sql := fmt.Sprintf(`DROP DATABASE IF EXISTS %s`, conf.DB.DBName)
|
sql := fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, conf.DB.DBName)
|
||||||
|
|
||||||
_, err = conn.Exec(ctx, sql)
|
_, err = conn.Exec(ctx, sql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -311,3 +311,13 @@ func getMigrationVars() map[string]interface{} {
|
|||||||
"env": strings.ToLower(os.Getenv("GO_ENV")),
|
"env": strings.ToLower(os.Getenv("GO_ENV")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initConfOnce() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if conf == nil {
|
||||||
|
if conf, err = initConf(); err != nil {
|
||||||
|
errlog.Fatal().Err(err).Msg("failed to read config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -90,8 +90,8 @@ func cmdNew(cmd *cobra.Command, args []string) {
|
|||||||
return os.Mkdir(p, os.ModePerm)
|
return os.Mkdir(p, os.ModePerm)
|
||||||
})
|
})
|
||||||
|
|
||||||
ifNotExists(path.Join(appMigrationsPath, "100_init.sql"), func(p string) error {
|
ifNotExists(path.Join(appMigrationsPath, "0_init.sql"), func(p string) error {
|
||||||
if v, err := tmpl.get("100_init.sql"); err == nil {
|
if v, err := tmpl.get("0_init.sql"); err == nil {
|
||||||
return ioutil.WriteFile(p, v, 0644)
|
return ioutil.WriteFile(p, v, 0644)
|
||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
|
@ -7,19 +7,22 @@ import (
|
|||||||
func cmdServ(cmd *cobra.Command, args []string) {
|
func cmdServ(cmd *cobra.Command, args []string) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
initWatcher(confPath)
|
||||||
|
|
||||||
if conf, err = initConf(); err != nil {
|
if conf, err = initConf(); err != nil {
|
||||||
errlog.Fatal().Err(err).Msg("failed to read config")
|
fatalInProd(err, "failed to read config")
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err = initDBPool(conf)
|
db, err = initDBPool(conf)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errlog.Fatal().Err(err).Msg("failed to connect to database")
|
fatalInProd(err, "failed to connect to database")
|
||||||
}
|
}
|
||||||
|
|
||||||
initCompiler()
|
initCompiler()
|
||||||
|
initResolvers()
|
||||||
initAllowList(confPath)
|
initAllowList(confPath)
|
||||||
initPreparedList()
|
initPreparedList(confPath)
|
||||||
initWatcher(confPath)
|
|
||||||
|
|
||||||
startHTTP()
|
startHTTP()
|
||||||
}
|
}
|
||||||
|
137
serv/config.go
137
serv/config.go
@ -33,7 +33,40 @@ type config struct {
|
|||||||
|
|
||||||
Inflections map[string]string
|
Inflections map[string]string
|
||||||
|
|
||||||
Auth struct {
|
Auth configAuth
|
||||||
|
Auths []configAuth
|
||||||
|
|
||||||
|
DB struct {
|
||||||
|
Type string
|
||||||
|
Host string
|
||||||
|
Port uint16
|
||||||
|
DBName string
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
Schema string
|
||||||
|
PoolSize int32 `mapstructure:"pool_size"`
|
||||||
|
MaxRetries int `mapstructure:"max_retries"`
|
||||||
|
SetUserID bool `mapstructure:"set_user_id"`
|
||||||
|
PingTimeout time.Duration `mapstructure:"ping_timeout"`
|
||||||
|
|
||||||
|
Vars map[string]string `mapstructure:"variables"`
|
||||||
|
Blocklist []string
|
||||||
|
|
||||||
|
Tables []configTable
|
||||||
|
} `mapstructure:"database"`
|
||||||
|
|
||||||
|
Actions []configAction
|
||||||
|
|
||||||
|
Tables []configTable
|
||||||
|
|
||||||
|
RolesQuery string `mapstructure:"roles_query"`
|
||||||
|
Roles []configRole
|
||||||
|
roles map[string]*configRole
|
||||||
|
abacEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type configAuth struct {
|
||||||
|
Name string
|
||||||
Type string
|
Type string
|
||||||
Cookie string
|
Cookie string
|
||||||
CredsInHeader bool `mapstructure:"creds_in_header"`
|
CredsInHeader bool `mapstructure:"creds_in_header"`
|
||||||
@ -56,37 +89,17 @@ type config struct {
|
|||||||
PubKeyFile string `mapstructure:"public_key_file"`
|
PubKeyFile string `mapstructure:"public_key_file"`
|
||||||
PubKeyType string `mapstructure:"public_key_type"`
|
PubKeyType string `mapstructure:"public_key_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Header struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Exists bool
|
||||||
}
|
}
|
||||||
|
|
||||||
DB struct {
|
|
||||||
Type string
|
|
||||||
Host string
|
|
||||||
Port uint16
|
|
||||||
DBName string
|
|
||||||
User string
|
|
||||||
Password string
|
|
||||||
Schema string
|
|
||||||
PoolSize int32 `mapstructure:"pool_size"`
|
|
||||||
MaxRetries int `mapstructure:"max_retries"`
|
|
||||||
SetUserID bool `mapstructure:"set_user_id"`
|
|
||||||
PingTimeout time.Duration `mapstructure:"ping_timeout"`
|
|
||||||
|
|
||||||
Vars map[string]string `mapstructure:"variables"`
|
|
||||||
Blocklist []string
|
|
||||||
|
|
||||||
Tables []configTable
|
|
||||||
} `mapstructure:"database"`
|
|
||||||
|
|
||||||
Tables []configTable
|
|
||||||
|
|
||||||
RolesQuery string `mapstructure:"roles_query"`
|
|
||||||
Roles []configRole
|
|
||||||
roles map[string]*configRole
|
|
||||||
abacEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type configColumn struct {
|
type configColumn struct {
|
||||||
Name string
|
Name string
|
||||||
|
Type string
|
||||||
ForeignKey string `mapstructure:"related_to"`
|
ForeignKey string `mapstructure:"related_to"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +168,12 @@ type configRole struct {
|
|||||||
tablesMap map[string]*configRoleTable
|
tablesMap map[string]*configRoleTable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type configAction struct {
|
||||||
|
Name string
|
||||||
|
SQL string
|
||||||
|
AuthName string `mapstructure:"auth_name"`
|
||||||
|
}
|
||||||
|
|
||||||
func newConfig(name string) *viper.Viper {
|
func newConfig(name string) *viper.Viper {
|
||||||
vi := viper.New()
|
vi := viper.New()
|
||||||
|
|
||||||
@ -282,26 +301,48 @@ func (c *config) Init(vi *viper.Viper) error {
|
|||||||
func (c *config) validate() {
|
func (c *config) validate() {
|
||||||
rm := make(map[string]struct{})
|
rm := make(map[string]struct{})
|
||||||
|
|
||||||
for i := range c.Roles {
|
for _, v := range c.Roles {
|
||||||
name := c.Roles[i].Name
|
name := strings.ToLower(v.Name)
|
||||||
|
|
||||||
if _, ok := rm[name]; ok {
|
if _, ok := rm[name]; ok {
|
||||||
errlog.Fatal().Msgf("duplicate config for role '%s'", c.Roles[i].Name)
|
errlog.Fatal().Msgf("duplicate config for role '%s'", v.Name)
|
||||||
}
|
}
|
||||||
rm[name] = struct{}{}
|
rm[name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
tm := make(map[string]struct{})
|
tm := make(map[string]struct{})
|
||||||
|
|
||||||
for i := range c.Tables {
|
for _, v := range c.Tables {
|
||||||
name := c.Tables[i].Name
|
name := strings.ToLower(v.Name)
|
||||||
|
|
||||||
if _, ok := tm[name]; ok {
|
if _, ok := tm[name]; ok {
|
||||||
errlog.Fatal().Msgf("duplicate config for table '%s'", c.Tables[i].Name)
|
errlog.Fatal().Msgf("duplicate config for table '%s'", v.Name)
|
||||||
}
|
}
|
||||||
tm[name] = struct{}{}
|
tm[name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
am := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, v := range c.Auths {
|
||||||
|
name := strings.ToLower(v.Name)
|
||||||
|
|
||||||
|
if _, ok := am[name]; ok {
|
||||||
|
errlog.Fatal().Msgf("duplicate config for auth '%s'", v.Name)
|
||||||
|
}
|
||||||
|
am[name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range c.Actions {
|
||||||
|
if len(v.AuthName) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
authName := strings.ToLower(v.AuthName)
|
||||||
|
|
||||||
|
if _, ok := am[authName]; !ok {
|
||||||
|
errlog.Fatal().Msgf("invalid auth_name for action '%s'", v.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(c.RolesQuery) == 0 {
|
if len(c.RolesQuery) == 0 {
|
||||||
logger.Warn().Msgf("no 'roles_query' defined.")
|
logger.Warn().Msgf("no 'roles_query' defined.")
|
||||||
}
|
}
|
||||||
@ -313,7 +354,7 @@ func (c *config) getAliasMap() map[string][]string {
|
|||||||
for i := range c.Tables {
|
for i := range c.Tables {
|
||||||
t := c.Tables[i]
|
t := c.Tables[i]
|
||||||
|
|
||||||
if len(t.Table) == 0 {
|
if len(t.Table) == 0 || len(t.Columns) != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,3 +389,31 @@ func sanitize(s string) string {
|
|||||||
return strings.ToLower(m)
|
return strings.ToLower(m)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getConfigName() string {
|
||||||
|
if len(os.Getenv("GO_ENV")) == 0 {
|
||||||
|
return "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
ge := strings.ToLower(os.Getenv("GO_ENV"))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(ge, "pro"):
|
||||||
|
return "prod"
|
||||||
|
|
||||||
|
case strings.HasPrefix(ge, "sta"):
|
||||||
|
return "stage"
|
||||||
|
|
||||||
|
case strings.HasPrefix(ge, "tes"):
|
||||||
|
return "test"
|
||||||
|
|
||||||
|
case strings.HasPrefix(ge, "dev"):
|
||||||
|
return "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ge
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDev() bool {
|
||||||
|
return strings.HasPrefix(os.Getenv("GO_ENV"), "dev")
|
||||||
|
}
|
||||||
|
@ -8,9 +8,61 @@ import (
|
|||||||
"github.com/dosco/super-graph/qcode"
|
"github.com/dosco/super-graph/qcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func addTables(c *config, di *psql.DBInfo) error {
|
||||||
|
for _, t := range c.Tables {
|
||||||
|
if len(t.Table) == 0 || len(t.Columns) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := addTable(di, t.Columns, t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTable(di *psql.DBInfo, cols []configColumn, t configTable) error {
|
||||||
|
bc, ok := di.GetColumn(t.Table, t.Name)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Column '%s' not found on table '%s'",
|
||||||
|
t.Name, t.Table)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.Type != "json" && bc.Type != "jsonb" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Column '%s' in table '%s' is of type '%s'. Only JSON or JSONB is valid",
|
||||||
|
t.Name, t.Table, bc.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
table := psql.DBTable{
|
||||||
|
Name: t.Name,
|
||||||
|
Key: strings.ToLower(t.Name),
|
||||||
|
Type: bc.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := make([]psql.DBColumn, 0, len(cols))
|
||||||
|
|
||||||
|
for i := range cols {
|
||||||
|
c := cols[i]
|
||||||
|
columns = append(columns, psql.DBColumn{
|
||||||
|
Name: c.Name,
|
||||||
|
Key: strings.ToLower(c.Name),
|
||||||
|
Type: c.Type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
di.AddTable(table, columns)
|
||||||
|
bc.FKeyTable = t.Name
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func addForeignKeys(c *config, di *psql.DBInfo) error {
|
func addForeignKeys(c *config, di *psql.DBInfo) error {
|
||||||
for _, t := range c.Tables {
|
for _, t := range c.Tables {
|
||||||
for _, c := range t.Columns {
|
for _, c := range t.Columns {
|
||||||
|
if len(c.ForeignKey) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := addForeignKey(di, c, t); err != nil {
|
if err := addForeignKey(di, c, t); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -23,7 +75,7 @@ func addForeignKey(di *psql.DBInfo, c configColumn, t configTable) error {
|
|||||||
c1, ok := di.GetColumn(t.Name, c.Name)
|
c1, ok := di.GetColumn(t.Name, c.Name)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"Invalid table '%s' or column '%s in config",
|
"Invalid table '%s' or column '%s' in config",
|
||||||
t.Name, c.Name)
|
t.Name, c.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cespare/xxhash/v2"
|
"github.com/cespare/xxhash/v2"
|
||||||
|
"github.com/dosco/super-graph/allow"
|
||||||
"github.com/dosco/super-graph/qcode"
|
"github.com/dosco/super-graph/qcode"
|
||||||
"github.com/jackc/pgx/v4"
|
"github.com/jackc/pgx/v4"
|
||||||
"github.com/valyala/fasttemplate"
|
"github.com/valyala/fasttemplate"
|
||||||
@ -107,7 +108,7 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *stmt, error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ps, ok := _preparedList[gqlHash(c.req.Query, c.req.Vars, role)]
|
ps, ok := _preparedList[stmtHash(allow.QueryName(c.req.Query), role)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil, errUnauthorized
|
return nil, nil, errUnauthorized
|
||||||
}
|
}
|
||||||
@ -240,8 +241,10 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !conf.Production {
|
if allowList.IsPersist() {
|
||||||
_allowList.add(&c.req)
|
if err := allowList.Add(c.req.Vars, c.req.Query, c.req.ref); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(stmts) > 1 {
|
if len(stmts) > 1 {
|
||||||
|
@ -4,7 +4,7 @@ package serv
|
|||||||
|
|
||||||
func Fuzz(data []byte) int {
|
func Fuzz(data []byte) int {
|
||||||
gql := string(data)
|
gql := string(data)
|
||||||
gqlName(gql)
|
QueryName(gql)
|
||||||
gqlHash(gql, nil, "")
|
gqlHash(gql, nil, "")
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
@ -10,7 +10,6 @@ func TestFuzzCrashers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range crashers {
|
for _, f := range crashers {
|
||||||
_ = gqlName(f)
|
|
||||||
gqlHash(f, nil, "")
|
gqlHash(f, nil, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
165
serv/init.go
Normal file
165
serv/init.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package serv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/dosco/super-graph/allow"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initLog() {
|
||||||
|
out := zerolog.ConsoleWriter{Out: os.Stderr}
|
||||||
|
logger = zerolog.New(out).With().Timestamp().Logger()
|
||||||
|
errlog = logger.With().Caller().Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConf() (*config, error) {
|
||||||
|
vi := newConfig(getConfigName())
|
||||||
|
|
||||||
|
if err := vi.ReadInConfig(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inherits := vi.GetString("inherits")
|
||||||
|
if len(inherits) != 0 {
|
||||||
|
vi = newConfig(inherits)
|
||||||
|
|
||||||
|
if err := vi.ReadInConfig(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if vi.IsSet("inherits") {
|
||||||
|
errlog.Fatal().Msgf("inherited config (%s) cannot itself inherit (%s)",
|
||||||
|
inherits,
|
||||||
|
vi.GetString("inherits"))
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.SetConfigName(getConfigName())
|
||||||
|
|
||||||
|
if err := vi.MergeInConfig(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &config{}
|
||||||
|
|
||||||
|
if err := c.Init(vi); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode config, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logLevel, err := zerolog.ParseLevel(c.LogLevel)
|
||||||
|
if err != nil {
|
||||||
|
errlog.Error().Err(err).Msg("error setting log_level")
|
||||||
|
}
|
||||||
|
zerolog.SetGlobalLevel(logLevel)
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDB(c *config, useDB bool) (*pgx.Conn, error) {
|
||||||
|
config, _ := pgx.ParseConfig("")
|
||||||
|
config.Host = c.DB.Host
|
||||||
|
config.Port = c.DB.Port
|
||||||
|
config.User = c.DB.User
|
||||||
|
config.Password = c.DB.Password
|
||||||
|
config.RuntimeParams = map[string]string{
|
||||||
|
"application_name": c.AppName,
|
||||||
|
"search_path": c.DB.Schema,
|
||||||
|
}
|
||||||
|
|
||||||
|
if useDB {
|
||||||
|
config.Database = c.DB.DBName
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.LogLevel {
|
||||||
|
case "debug":
|
||||||
|
config.LogLevel = pgx.LogLevelDebug
|
||||||
|
case "info":
|
||||||
|
config.LogLevel = pgx.LogLevelInfo
|
||||||
|
case "warn":
|
||||||
|
config.LogLevel = pgx.LogLevelWarn
|
||||||
|
case "error":
|
||||||
|
config.LogLevel = pgx.LogLevelError
|
||||||
|
default:
|
||||||
|
config.LogLevel = pgx.LogLevelNone
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Logger = NewSQLLogger(logger)
|
||||||
|
|
||||||
|
db, err := pgx.ConnectConfig(context.Background(), config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDBPool(c *config) (*pgxpool.Pool, error) {
|
||||||
|
config, _ := pgxpool.ParseConfig("")
|
||||||
|
config.ConnConfig.Host = c.DB.Host
|
||||||
|
config.ConnConfig.Port = c.DB.Port
|
||||||
|
config.ConnConfig.Database = c.DB.DBName
|
||||||
|
config.ConnConfig.User = c.DB.User
|
||||||
|
config.ConnConfig.Password = c.DB.Password
|
||||||
|
config.ConnConfig.RuntimeParams = map[string]string{
|
||||||
|
"application_name": c.AppName,
|
||||||
|
"search_path": c.DB.Schema,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.LogLevel {
|
||||||
|
case "debug":
|
||||||
|
config.ConnConfig.LogLevel = pgx.LogLevelDebug
|
||||||
|
case "info":
|
||||||
|
config.ConnConfig.LogLevel = pgx.LogLevelInfo
|
||||||
|
case "warn":
|
||||||
|
config.ConnConfig.LogLevel = pgx.LogLevelWarn
|
||||||
|
case "error":
|
||||||
|
config.ConnConfig.LogLevel = pgx.LogLevelError
|
||||||
|
default:
|
||||||
|
config.ConnConfig.LogLevel = pgx.LogLevelNone
|
||||||
|
}
|
||||||
|
|
||||||
|
config.ConnConfig.Logger = NewSQLLogger(logger)
|
||||||
|
|
||||||
|
// if c.DB.MaxRetries != 0 {
|
||||||
|
// opt.MaxRetries = c.DB.MaxRetries
|
||||||
|
// }
|
||||||
|
|
||||||
|
if c.DB.PoolSize != 0 {
|
||||||
|
config.MaxConns = conf.DB.PoolSize
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := pgxpool.ConnectConfig(context.Background(), config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initCompiler() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
qcompile, pcompile, err = initCompilers(conf)
|
||||||
|
if err != nil {
|
||||||
|
errlog.Fatal().Err(err).Msg("failed to initialize compilers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initAllowList(cpath string) {
|
||||||
|
var ac allow.Config
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if !conf.Production {
|
||||||
|
ac = allow.Config{CreateIfNotExists: true, Persist: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
allowList, err = allow.New(cpath, ac)
|
||||||
|
if err != nil {
|
||||||
|
errlog.Fatal().Err(err).Msg("failed to initialize allow list")
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/dosco/super-graph/allow"
|
||||||
"github.com/dosco/super-graph/qcode"
|
"github.com/dosco/super-graph/qcode"
|
||||||
"github.com/jackc/pgconn"
|
"github.com/jackc/pgconn"
|
||||||
"github.com/jackc/pgx/v4"
|
"github.com/jackc/pgx/v4"
|
||||||
@ -23,7 +24,10 @@ var (
|
|||||||
_preparedList map[string]*preparedItem
|
_preparedList map[string]*preparedItem
|
||||||
)
|
)
|
||||||
|
|
||||||
func initPreparedList() {
|
func initPreparedList(cpath string) {
|
||||||
|
if allowList.IsPersist() {
|
||||||
|
return
|
||||||
|
}
|
||||||
_preparedList = make(map[string]*preparedItem)
|
_preparedList = make(map[string]*preparedItem)
|
||||||
|
|
||||||
tx, err := db.Begin(context.Background())
|
tx, err := db.Begin(context.Background())
|
||||||
@ -43,30 +47,38 @@ func initPreparedList() {
|
|||||||
|
|
||||||
success := 0
|
success := 0
|
||||||
|
|
||||||
for _, v := range _allowList.list {
|
list, err := allowList.Load()
|
||||||
if len(v.gql) == 0 {
|
if err != nil {
|
||||||
|
errlog.Fatal().Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range list {
|
||||||
|
if len(v.Query) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err := prepareStmt(v.gql, v.vars)
|
err := prepareStmt(v)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
success++
|
success++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v.vars) == 0 {
|
if len(v.Vars) == 0 {
|
||||||
logger.Warn().Err(err).Msg(v.gql)
|
logger.Warn().Err(err).Msg(v.Query)
|
||||||
} else {
|
} else {
|
||||||
logger.Warn().Err(err).Msgf("%s %s", v.vars, v.gql)
|
logger.Warn().Err(err).Msgf("%s %s", v.Vars, v.Query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().
|
logger.Info().
|
||||||
Msgf("Registered %d of %d queries from allow.list as prepared statements",
|
Msgf("Registered %d of %d queries from allow.list as prepared statements",
|
||||||
success, len(_allowList.list))
|
success, len(list))
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareStmt(gql string, vars []byte) error {
|
func prepareStmt(item allow.Item) error {
|
||||||
|
gql := item.Query
|
||||||
|
vars := item.Vars
|
||||||
|
|
||||||
qt := qcode.GetQType(gql)
|
qt := qcode.GetQType(gql)
|
||||||
q := []byte(gql)
|
q := []byte(gql)
|
||||||
|
|
||||||
@ -99,7 +111,7 @@ func prepareStmt(gql string, vars []byte) error {
|
|||||||
|
|
||||||
logger.Debug().Msg("Prepared statement role: user")
|
logger.Debug().Msg("Prepared statement role: user")
|
||||||
|
|
||||||
err = prepare(tx, stmts1, gqlHash(gql, vars, "user"))
|
err = prepare(tx, stmts1, stmtHash(item.Name, "user"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -112,7 +124,7 @@ func prepareStmt(gql string, vars []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = prepare(tx, stmts2, gqlHash(gql, vars, "anon"))
|
err = prepare(tx, stmts2, stmtHash(item.Name, "anon"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -127,7 +139,7 @@ func prepareStmt(gql string, vars []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = prepare(tx, stmts, gqlHash(gql, vars, role.Name))
|
err = prepare(tx, stmts, stmtHash(item.Name, role.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -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
|
// Ensure that we use the correct events, as they are not uniform across
|
||||||
// platforms. See https://github.com/fsnotify/fsnotify/issues/74
|
// platforms. See https://github.com/fsnotify/fsnotify/issues/74
|
||||||
|
|
||||||
if !conf.Production && strings.HasSuffix(event.Name, "/allow.list") {
|
if conf != nil && !conf.Production && strings.HasSuffix(event.Name, "/allow.list") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,16 +22,20 @@ type resolvFn struct {
|
|||||||
Fn func(h http.Header, id []byte) ([]byte, error)
|
Fn func(h http.Header, id []byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initResolvers() error {
|
func initResolvers() {
|
||||||
|
var err error
|
||||||
rmap = make(map[uint64]*resolvFn)
|
rmap = make(map[uint64]*resolvFn)
|
||||||
|
|
||||||
for _, t := range conf.Tables {
|
for _, t := range conf.Tables {
|
||||||
err := initRemotes(t)
|
err = initRemotes(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
if err != nil {
|
||||||
|
errlog.Fatal().Err(err).Msg("failed to initialize resolvers")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initRemotes(t configTable) error {
|
func initRemotes(t configTable) error {
|
118
serv/serv.go
118
serv/serv.go
@ -21,11 +21,15 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = addTables(c, di); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err = addForeignKeys(c, di); err != nil {
|
if err = addForeignKeys(c, di); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
schema, err = psql.NewDBSchema(db, di, c.getAliasMap())
|
schema, err = psql.NewDBSchema(di, c.getAliasMap())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -50,7 +54,7 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initWatcher(cpath string) {
|
func initWatcher(cpath string) {
|
||||||
if !conf.WatchAndReload {
|
if conf != nil && !conf.WatchAndReload {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,8 +74,17 @@ func initWatcher(cpath string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startHTTP() {
|
func startHTTP() {
|
||||||
|
var hostPort string
|
||||||
|
var appName string
|
||||||
|
|
||||||
|
defaultHP := "0.0.0.0:8080"
|
||||||
|
env := os.Getenv("GO_ENV")
|
||||||
|
|
||||||
|
if conf != nil {
|
||||||
|
appName = conf.AppName
|
||||||
hp := strings.SplitN(conf.HostPort, ":", 2)
|
hp := strings.SplitN(conf.HostPort, ":", 2)
|
||||||
|
|
||||||
|
if len(hp) == 2 {
|
||||||
if len(conf.Host) != 0 {
|
if len(conf.Host) != 0 {
|
||||||
hp[0] = conf.Host
|
hp[0] = conf.Host
|
||||||
}
|
}
|
||||||
@ -80,11 +93,22 @@ func startHTTP() {
|
|||||||
hp[1] = conf.Port
|
hp[1] = conf.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
hostPort := fmt.Sprintf("%s:%s", hp[0], hp[1])
|
hostPort = fmt.Sprintf("%s:%s", hp[0], hp[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hostPort) == 0 {
|
||||||
|
hostPort = defaultHP
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := routeHandler()
|
||||||
|
if err != nil {
|
||||||
|
errlog.Fatal().Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: hostPort,
|
Addr: hostPort,
|
||||||
Handler: routeHandler(),
|
Handler: routes,
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
MaxHeaderBytes: 1 << 20,
|
MaxHeaderBytes: 1 << 20,
|
||||||
@ -110,8 +134,8 @@ func startHTTP() {
|
|||||||
Str("version", version).
|
Str("version", version).
|
||||||
Str("git_branch", gitBranch).
|
Str("git_branch", gitBranch).
|
||||||
Str("host_post", hostPort).
|
Str("host_post", hostPort).
|
||||||
Str("app_name", conf.AppName).
|
Str("app_name", appName).
|
||||||
Str("env", conf.Env).
|
Str("env", env).
|
||||||
Msgf("%s listening", serverName)
|
Msgf("%s listening", serverName)
|
||||||
|
|
||||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||||
@ -121,22 +145,35 @@ func startHTTP() {
|
|||||||
<-idleConnsClosed
|
<-idleConnsClosed
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeHandler() http.Handler {
|
func routeHandler() (http.Handler, error) {
|
||||||
var apiH http.Handler
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
if conf.HTTPGZip {
|
if conf == nil {
|
||||||
gzipH := gziphandler.MustNewGzipLevelHandler(6)
|
return mux, nil
|
||||||
apiH = gzipH(http.HandlerFunc(apiV1))
|
|
||||||
} else {
|
|
||||||
apiH = http.HandlerFunc(apiV1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
routes := map[string]http.Handler{
|
||||||
mux.HandleFunc("/health", health)
|
"/health": http.HandlerFunc(health),
|
||||||
mux.Handle("/api/v1/graphql", withAuth(apiH))
|
"/api/v1/graphql": withAuth(http.HandlerFunc(apiV1), conf.Auth),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setActionRoutes(routes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if conf.WebUI {
|
if conf.WebUI {
|
||||||
mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox()))
|
routes["/"] = http.FileServer(rice.MustFindBox("../web/build").HTTPBox())
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.HTTPGZip {
|
||||||
|
gz := gziphandler.MustNewGzipLevelHandler(6)
|
||||||
|
for k, v := range routes {
|
||||||
|
routes[k] = gz(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range routes {
|
||||||
|
mux.Handle(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -144,29 +181,38 @@ func routeHandler() http.Handler {
|
|||||||
mux.ServeHTTP(w, r)
|
mux.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.HandlerFunc(fn)
|
return http.HandlerFunc(fn), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigName() string {
|
func setActionRoutes(routes map[string]http.Handler) error {
|
||||||
if len(os.Getenv("GO_ENV")) == 0 {
|
var err error
|
||||||
return "dev"
|
|
||||||
|
for _, a := range conf.Actions {
|
||||||
|
var fn http.Handler
|
||||||
|
|
||||||
|
fn, err = newAction(a)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
ge := strings.ToLower(os.Getenv("GO_ENV"))
|
p := fmt.Sprintf("/api/v1/actions/%s", strings.ToLower(a.Name))
|
||||||
|
|
||||||
switch {
|
if authc, ok := findAuth(a.AuthName); ok {
|
||||||
case strings.HasPrefix(ge, "pro"):
|
routes[p] = withAuth(fn, authc)
|
||||||
return "prod"
|
} else {
|
||||||
|
routes[p] = fn
|
||||||
case strings.HasPrefix(ge, "sta"):
|
|
||||||
return "stage"
|
|
||||||
|
|
||||||
case strings.HasPrefix(ge, "tes"):
|
|
||||||
return "test"
|
|
||||||
|
|
||||||
case strings.HasPrefix(ge, "dev"):
|
|
||||||
return "dev"
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return ge
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAuth(name string) (configAuth, bool) {
|
||||||
|
var authc configAuth
|
||||||
|
|
||||||
|
for _, a := range conf.Auths {
|
||||||
|
if strings.EqualFold(a.Name, name) {
|
||||||
|
return a, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authc, false
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/cespare/xxhash/v2"
|
"github.com/cespare/xxhash/v2"
|
||||||
"github.com/dosco/super-graph/jsn"
|
"github.com/dosco/super-graph/jsn"
|
||||||
@ -22,6 +23,14 @@ func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint: errcheck
|
||||||
|
func stmtHash(name string, role string) string {
|
||||||
|
h := sha1.New()
|
||||||
|
io.WriteString(h, strings.ToLower(name))
|
||||||
|
io.WriteString(h, role)
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
// nolint: errcheck
|
// nolint: errcheck
|
||||||
func gqlHash(b string, vars []byte, role string) string {
|
func gqlHash(b string, vars []byte, role string) string {
|
||||||
b = strings.TrimSpace(b)
|
b = strings.TrimSpace(b)
|
||||||
@ -108,30 +117,6 @@ func al(b byte) bool {
|
|||||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||||
}
|
}
|
||||||
|
|
||||||
func gqlName(b string) string {
|
|
||||||
state, s := 0, 0
|
|
||||||
|
|
||||||
for i := 0; i < len(b); i++ {
|
|
||||||
switch {
|
|
||||||
case state == 2 && b[i] == '{':
|
|
||||||
return b[s:i]
|
|
||||||
case state == 2 && b[i] == ' ':
|
|
||||||
return b[s:i]
|
|
||||||
case state == 1 && b[i] == '{':
|
|
||||||
return ""
|
|
||||||
case state == 1 && b[i] != ' ':
|
|
||||||
s = i
|
|
||||||
state = 2
|
|
||||||
case state == 1 && b[i] == ' ':
|
|
||||||
continue
|
|
||||||
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
|
|
||||||
state = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func findStmt(role string, stmts []stmt) *stmt {
|
func findStmt(role string, stmts []stmt) *stmt {
|
||||||
for i := range stmts {
|
for i := range stmts {
|
||||||
if stmts[i].role.Name != role {
|
if stmts[i].role.Name != role {
|
||||||
@ -141,3 +126,16 @@ func findStmt(role string, stmts []stmt) *stmt {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fatalInProd(err error, msg string) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
if !isDev() {
|
||||||
|
errlog.Fatal().Err(err).Msg(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
errlog.Error().Err(err).Msg(msg)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
@ -229,80 +229,3 @@ func TestGQLHashWithVars2(t *testing.T) {
|
|||||||
t.Fatal("Hashes don't match they should")
|
t.Fatal("Hashes don't match they should")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGQLName1(t *testing.T) {
|
|
||||||
var q = `
|
|
||||||
query {
|
|
||||||
products(
|
|
||||||
distinct: [price]
|
|
||||||
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
|
|
||||||
) { id name } }`
|
|
||||||
|
|
||||||
name := gqlName(q)
|
|
||||||
|
|
||||||
if len(name) != 0 {
|
|
||||||
t.Fatal("Name should be empty, not ", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGQLName2(t *testing.T) {
|
|
||||||
var q = `
|
|
||||||
query hakuna_matata {
|
|
||||||
products(
|
|
||||||
distinct: [price]
|
|
||||||
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
name := gqlName(q)
|
|
||||||
|
|
||||||
if name != "hakuna_matata" {
|
|
||||||
t.Fatal("Name should be 'hakuna_matata', not ", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGQLName3(t *testing.T) {
|
|
||||||
var q = `
|
|
||||||
mutation means{ users { id } }`
|
|
||||||
|
|
||||||
// var v2 = ` { products( limit: 30, order_by: { price: desc }, distinct: [ price ] where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) { id name price user { id email } } } `
|
|
||||||
|
|
||||||
name := gqlName(q)
|
|
||||||
|
|
||||||
if name != "means" {
|
|
||||||
t.Fatal("Name should be 'means', not ", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGQLName4(t *testing.T) {
|
|
||||||
var q = `
|
|
||||||
query no_worries
|
|
||||||
users {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
name := gqlName(q)
|
|
||||||
|
|
||||||
if name != "no_worries" {
|
|
||||||
t.Fatal("Name should be 'no_worries', not ", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGQLName5(t *testing.T) {
|
|
||||||
var q = `
|
|
||||||
{
|
|
||||||
users {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
name := gqlName(q)
|
|
||||||
|
|
||||||
if len(name) != 0 {
|
|
||||||
t.Fatal("Name should be empty, not ", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
35
tmpl/dev.yml
35
tmpl/dev.yml
@ -49,7 +49,7 @@ migrations_path: ./config/migrations
|
|||||||
# sheep: sheep
|
# sheep: sheep
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
# Can be 'rails' or 'jwt'
|
# Can be 'rails', 'jwt' or 'header'
|
||||||
type: rails
|
type: rails
|
||||||
cookie: _{% app_name_slug %}_session
|
cookie: _{% app_name_slug %}_session
|
||||||
|
|
||||||
@ -83,6 +83,22 @@ auth:
|
|||||||
# public_key_file: /secrets/public_key.pem
|
# public_key_file: /secrets/public_key.pem
|
||||||
# public_key_type: ecdsa #rsa
|
# public_key_type: ecdsa #rsa
|
||||||
|
|
||||||
|
# header:
|
||||||
|
# name: dnt
|
||||||
|
# exists: true
|
||||||
|
# value: localhost:8080
|
||||||
|
|
||||||
|
# You can add additional named auths to use with actions
|
||||||
|
# In this example actions using this auth can only be
|
||||||
|
# called from the Google Appengine Cron service that
|
||||||
|
# sets a special header to all it's requests
|
||||||
|
auths:
|
||||||
|
- name: from_taskqueue
|
||||||
|
type: header
|
||||||
|
header:
|
||||||
|
name: X-Appengine-Cron
|
||||||
|
exists: true
|
||||||
|
|
||||||
database:
|
database:
|
||||||
type: postgres
|
type: postgres
|
||||||
host: db
|
host: db
|
||||||
@ -116,6 +132,16 @@ database:
|
|||||||
- encrypted
|
- encrypted
|
||||||
- token
|
- token
|
||||||
|
|
||||||
|
# Create custom actions with their own api endpoints
|
||||||
|
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
|
||||||
|
# A request to this url will execute the configured SQL query
|
||||||
|
# which in this case refreshes a materialized view in the database.
|
||||||
|
# The auth_name is from one of the configured auths
|
||||||
|
actions:
|
||||||
|
- name: refresh_leaderboard_users
|
||||||
|
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||||
|
auth_name: from_taskqueue
|
||||||
|
|
||||||
tables:
|
tables:
|
||||||
- name: customers
|
- name: customers
|
||||||
remotes:
|
remotes:
|
||||||
@ -137,6 +163,7 @@ tables:
|
|||||||
name: me
|
name: me
|
||||||
table: users
|
table: users
|
||||||
|
|
||||||
|
|
||||||
roles_query: "SELECT * FROM users WHERE id = $user_id"
|
roles_query: "SELECT * FROM users WHERE id = $user_id"
|
||||||
|
|
||||||
roles:
|
roles:
|
||||||
@ -168,20 +195,16 @@ roles:
|
|||||||
query:
|
query:
|
||||||
limit: 50
|
limit: 50
|
||||||
filters: ["{ user_id: { eq: $user_id } }"]
|
filters: ["{ user_id: { eq: $user_id } }"]
|
||||||
columns: ["id", "name", "description" ]
|
|
||||||
disable_functions: false
|
disable_functions: false
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
filters: ["{ user_id: { eq: $user_id } }"]
|
filters: ["{ user_id: { eq: $user_id } }"]
|
||||||
columns: ["id", "name", "description" ]
|
|
||||||
presets:
|
presets:
|
||||||
|
- user_id: "$user_id"
|
||||||
- created_at: "now"
|
- created_at: "now"
|
||||||
|
|
||||||
update:
|
update:
|
||||||
filters: ["{ user_id: { eq: $user_id } }"]
|
filters: ["{ user_id: { eq: $user_id } }"]
|
||||||
columns:
|
|
||||||
- id
|
|
||||||
- name
|
|
||||||
presets:
|
presets:
|
||||||
- updated_at: "now"
|
- updated_at: "now"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user