Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
4ffa1483a4 | |||
52f3b1c7a2 | |||
2d466bfb12 | |||
a0b8907c3c | |||
8097ca3b8f | |||
0e498b0e94 |
@ -137,7 +137,7 @@
|
||||
</h1>
|
||||
<div class="text-2xl md:text-3xl">
|
||||
<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>
|
||||
<pre>‣ docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed</pre>
|
||||
|
@ -31,14 +31,14 @@ Super Graph has a rich feature set like integrating with your existing Ruby on R
|
||||
## Try the demo app
|
||||
|
||||
```bash
|
||||
# download the Docker compose config for the demo
|
||||
curl -L -o demo.yml https://bit.ly/2mq05lW
|
||||
# clone the repository
|
||||
git clone https://github.com/dosco/super-graph
|
||||
|
||||
# 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
|
||||
docker-compose -f demo.yml up
|
||||
docker-compose up
|
||||
|
||||
# signin to the demo app (user1@demo.com / 123456)
|
||||
open http://localhost:3000
|
||||
@ -906,7 +906,7 @@ mutation {
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Updates
|
||||
### Nested Update
|
||||
|
||||
Update a product item first and then assign it to a user
|
||||
|
||||
@ -966,7 +966,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
|
||||
and better client side code.
|
||||
@ -988,6 +988,70 @@ fetch('http://localhost:8080/api/v1/graphql', {
|
||||
.then(res => console.log(res.data));
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
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
|
14
jsn/get.go
14
jsn/get.go
@ -130,6 +130,20 @@ func Get(b []byte, keys [][]byte) []Field {
|
||||
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
|
||||
e = 0
|
||||
}
|
||||
|
@ -9,11 +9,11 @@ var (
|
||||
input1 = `
|
||||
{
|
||||
"data": {
|
||||
"test": { "__twitter_id": "ABCD" },
|
||||
"test_1a": { "__twitter_id": "ABCD" },
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"full_name": "'Sidney Stroman'",
|
||||
"full_name": "'Sidney St[1]roman'",
|
||||
"email": "user0@demo.com",
|
||||
"__twitter_id": "2048666903444506956",
|
||||
"embed": {
|
||||
@ -108,7 +108,7 @@ var (
|
||||
input2 = `
|
||||
[{
|
||||
"id": 1,
|
||||
"full_name": "Sidney Stroman",
|
||||
"full_name": "Sidney St[1]roman",
|
||||
"email": "user0@demo.com",
|
||||
"__twitter_id": "2048666903444506956",
|
||||
"something": null,
|
||||
@ -130,7 +130,7 @@ var (
|
||||
input3 = `
|
||||
{
|
||||
"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}]
|
||||
}
|
||||
}`
|
||||
@ -138,7 +138,7 @@ var (
|
||||
input4 = `
|
||||
{ "users" : [{
|
||||
"id": 1,
|
||||
"full_name": "Sidney Stroman",
|
||||
"full_name": "Sidney St[1]roman",
|
||||
"email": "user0@demo.com",
|
||||
"__twitter_id": "2048666903444506956",
|
||||
"embed": {
|
||||
@ -155,24 +155,26 @@ var (
|
||||
"email": "user1@demo.com",
|
||||
"__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) {
|
||||
values := Get([]byte(input1), [][]byte{
|
||||
[]byte("test_1a"),
|
||||
[]byte("__twitter_id"),
|
||||
[]byte("work_email"),
|
||||
})
|
||||
|
||||
expected := []Field{
|
||||
{[]byte("test_1a"), []byte(`{ "__twitter_id": "ABCD" }`)},
|
||||
{[]byte("__twitter_id"), []byte(`"ABCD"`)},
|
||||
{[]byte("__twitter_id"), []byte(`"2048666903444506956"`)},
|
||||
{[]byte("__twitter_id"), []byte(`"ABC123"`)},
|
||||
{[]byte("__twitter_id"), []byte(`"more123"`)},
|
||||
{[]byte("__twitter_id"),
|
||||
[]byte(`[{ "name": "hello" }, { "name": "world"}]`)},
|
||||
{[]byte("__twitter_id"),
|
||||
[]byte(`{ "name": "\"hellos\"", "address": { "work": "1 infinity loop" } }`),
|
||||
},
|
||||
{[]byte("__twitter_id"), []byte(`[{ "name": "hello" }, { "name": "world"}]`)},
|
||||
{[]byte("__twitter_id"), []byte(`{ "name": "\"hellos\"", "address": { "work": "1 infinity loop" } }`)},
|
||||
{[]byte("__twitter_id"), []byte(`1234567890`)},
|
||||
{[]byte("__twitter_id"), []byte(`1.23E`)},
|
||||
{[]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) {
|
||||
v1 := []byte("12345")
|
||||
if !bytes.Equal(Value(v1), v1) {
|
||||
@ -230,7 +256,7 @@ func TestFilter1(t *testing.T) {
|
||||
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 {
|
||||
t.Error("Does not match expected json")
|
||||
@ -306,7 +332,7 @@ func TestReplace(t *testing.T) {
|
||||
|
||||
expected := `{ "users" : [{
|
||||
"id": 1,
|
||||
"full_name": "Sidney Stroman",
|
||||
"full_name": "Sidney St[1]roman",
|
||||
"email": "user0@demo.com",
|
||||
"__twitter_id": "2048666903444506956",
|
||||
"embed": {
|
||||
@ -338,7 +364,7 @@ func TestReplace(t *testing.T) {
|
||||
func TestReplaceEmpty(t *testing.T) {
|
||||
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{})
|
||||
if err != nil {
|
||||
@ -395,7 +421,7 @@ func TestKeys3(t *testing.T) {
|
||||
json := `{
|
||||
"insert": {
|
||||
"created_at": "now",
|
||||
"test": { "type1": "a", "type2": "b" },
|
||||
"test_1a": { "type1": "a", "type2": "b" },
|
||||
"name": "Hello",
|
||||
"updated_at": "now",
|
||||
"description": "World"
|
||||
@ -406,7 +432,7 @@ func TestKeys3(t *testing.T) {
|
||||
fields := Keys([]byte(json))
|
||||
|
||||
exp := []string{
|
||||
"insert", "created_at", "test", "type1", "type2", "name", "updated_at", "description",
|
||||
"insert", "created_at", "test_1a", "type1", "type2", "name", "updated_at", "description",
|
||||
"user",
|
||||
}
|
||||
|
||||
|
13
jsn/keys.go
13
jsn/keys.go
@ -111,6 +111,19 @@ func Keys(b []byte) [][]byte {
|
||||
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
|
||||
k = nil
|
||||
e = 0
|
||||
|
@ -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{
|
||||
"data": json.RawMessage(`{
|
||||
@ -278,6 +278,10 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
|
||||
product(insert: $data) {
|
||||
id
|
||||
name
|
||||
tags {
|
||||
id
|
||||
name
|
||||
}
|
||||
user {
|
||||
id
|
||||
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), "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", "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", "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{
|
||||
"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), "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", "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"`
|
||||
|
||||
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) {
|
||||
t.Run("simpleInsert", simpleInsert)
|
||||
t.Run("singleInsert", singleInsert)
|
||||
@ -320,4 +361,6 @@ func TestCompileInsert(t *testing.T) {
|
||||
t.Run("nestedInsertOneToOne", nestedInsertOneToOne)
|
||||
t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect)
|
||||
t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect)
|
||||
t.Run("nestedInsertOneToOneWithConnectArray", nestedInsertOneToOneWithConnectArray)
|
||||
|
||||
}
|
||||
|
144
psql/mutate.go
144
psql/mutate.go
@ -101,6 +101,9 @@ type renitem struct {
|
||||
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 {
|
||||
var data map[string]json.RawMessage
|
||||
var array bool
|
||||
@ -124,9 +127,6 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
|
||||
if v[0] != '{' && v[0] != '[' {
|
||||
continue
|
||||
}
|
||||
if _, ok := item.ti.ColMap[k]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get child-to-parent relationship
|
||||
relCP, err := c.schema.GetRel(k, item.key)
|
||||
@ -152,13 +152,9 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
|
||||
id++
|
||||
}
|
||||
|
||||
} else {
|
||||
ti, err := c.schema.GetTable(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
@ -277,8 +273,12 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
||||
io.WriteString(w, ` SET `)
|
||||
quoted(w, item.relPC.Right.Col)
|
||||
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)
|
||||
io.WriteString(w, `FROM `)
|
||||
|
||||
io.WriteString(w, ` FROM `)
|
||||
quoted(w, item.relPC.Left.Table)
|
||||
io.WriteString(w, ` WHERE`)
|
||||
|
||||
@ -290,7 +290,7 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
||||
} else {
|
||||
io.WriteString(w, ` (`)
|
||||
}
|
||||
if err := renderKVItemWhere(w, v); err != nil {
|
||||
if err := renderWhereFromJSON(w, v, "connect", v.val); err != nil {
|
||||
return err
|
||||
}
|
||||
io.WriteString(w, `)`)
|
||||
@ -313,7 +313,19 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
||||
quoted(w, item.ti.Name)
|
||||
io.WriteString(w, ` SET `)
|
||||
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 `)
|
||||
quoted(w, item.relPC.Left.Table)
|
||||
io.WriteString(w, ` WHERE`)
|
||||
@ -326,7 +338,7 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error {
|
||||
} else {
|
||||
io.WriteString(w, ` (`)
|
||||
}
|
||||
if err := renderKVItemWhere(w, v); err != nil {
|
||||
if err := renderWhereFromJSON(w, v, "disconnect", v.val); err != nil {
|
||||
return err
|
||||
}
|
||||
io.WriteString(w, `)`)
|
||||
@ -514,12 +526,24 @@ func (c *compilerContext) renderConnectStmt(qc *qcode.QCode, w io.Writer,
|
||||
|
||||
io.WriteString(w, `, `)
|
||||
quoted(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)
|
||||
|
||||
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
|
||||
}
|
||||
io.WriteString(c.w, ` LIMIT 1)`)
|
||||
@ -540,43 +564,95 @@ func (c *compilerContext) renderDisconnectStmt(qc *qcode.QCode, w io.Writer,
|
||||
quoted(w, item.ti.Name)
|
||||
io.WriteString(c.w, ` AS (`)
|
||||
|
||||
io.WriteString(c.w, `SELECT * FROM (VALUES(NULL::`)
|
||||
io.WriteString(w, rel.Right.col.Type)
|
||||
io.WriteString(c.w, `)) AS LOOKUP(`)
|
||||
quoted(w, rel.Right.Col)
|
||||
io.WriteString(c.w, `))`)
|
||||
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(w, rel.Right.col.Type)
|
||||
io.WriteString(c.w, `)) AS LOOKUP(`)
|
||||
quoted(w, rel.Right.Col)
|
||||
io.WriteString(c.w, `))`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderKVItemWhere(w io.Writer, item kvitem) error {
|
||||
return renderWhereFromJSON(w, item.ti.Name, item.val)
|
||||
}
|
||||
|
||||
func renderWhereFromJSON(w io.Writer, table string, val []byte) error {
|
||||
func renderWhereFromJSON(w io.Writer, item kvitem, key string, val []byte) error {
|
||||
var kv map[string]json.RawMessage
|
||||
ti := item.ti
|
||||
|
||||
if err := json.Unmarshal(val, &kv); err != nil {
|
||||
return err
|
||||
}
|
||||
i := 0
|
||||
for k, v := range kv {
|
||||
col, ok := ti.ColMap[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if i != 0 {
|
||||
io.WriteString(w, ` AND `)
|
||||
}
|
||||
colWithTable(w, table, k)
|
||||
io.WriteString(w, ` = '`)
|
||||
switch v[0] {
|
||||
case '"':
|
||||
w.Write(v[1 : len(v)-1])
|
||||
default:
|
||||
w.Write(v)
|
||||
|
||||
if v[0] == '[' {
|
||||
colWithTable(w, ti.Name, k)
|
||||
|
||||
if col.Array {
|
||||
io.WriteString(w, ` && `)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
io.WriteString(w, `'`)
|
||||
|
||||
i++
|
||||
}
|
||||
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 {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, item.ti.Name)
|
||||
|
@ -134,6 +134,7 @@ func TestMain(m *testing.M) {
|
||||
DBTable{Name: "products", Type: "table"},
|
||||
DBTable{Name: "purchases", Type: "table"},
|
||||
DBTable{Name: "tags", Type: "table"},
|
||||
DBTable{Name: "tag_count", Type: "json"},
|
||||
}
|
||||
|
||||
columns := [][]DBColumn{
|
||||
@ -169,7 +170,8 @@ func TestMain(m *testing.M) {
|
||||
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: "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}},
|
||||
@ -182,6 +184,9 @@ func TestMain(m *testing.M) {
|
||||
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 {
|
||||
|
203
psql/query.go
203
psql/query.go
@ -82,17 +82,21 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
multiRoot := (len(qc.Roots) > 1)
|
||||
|
||||
st := NewIntStack()
|
||||
si := 0
|
||||
|
||||
if multiRoot {
|
||||
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]
|
||||
if root.SkipRender {
|
||||
continue
|
||||
}
|
||||
|
||||
st.Push(root.ID + closeBlock)
|
||||
st.Push(root.ID)
|
||||
|
||||
if i != 0 {
|
||||
if si != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
|
||||
@ -103,24 +107,34 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
io.WriteString(c.w, `"`)
|
||||
|
||||
alias(c.w, root.FieldName)
|
||||
si++
|
||||
}
|
||||
|
||||
io.WriteString(c.w, ` FROM `)
|
||||
if si != 0 {
|
||||
io.WriteString(c.w, ` FROM `)
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
root := qc.Selects[0]
|
||||
if !root.SkipRender {
|
||||
io.WriteString(c.w, `SELECT json_object_agg(`)
|
||||
io.WriteString(c.w, `'`)
|
||||
io.WriteString(c.w, root.FieldName)
|
||||
io.WriteString(c.w, `', `)
|
||||
io.WriteString(c.w, `json_`)
|
||||
int2string(c.w, root.ID)
|
||||
|
||||
io.WriteString(c.w, `SELECT json_object_agg(`)
|
||||
io.WriteString(c.w, `'`)
|
||||
io.WriteString(c.w, root.FieldName)
|
||||
io.WriteString(c.w, `', `)
|
||||
io.WriteString(c.w, `json_`)
|
||||
int2string(c.w, root.ID)
|
||||
st.Push(root.ID + closeBlock)
|
||||
st.Push(root.ID)
|
||||
|
||||
st.Push(root.ID + closeBlock)
|
||||
st.Push(root.ID)
|
||||
io.WriteString(c.w, `) FROM `)
|
||||
si++
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `) FROM `)
|
||||
if si == 0 {
|
||||
return 0, errors.New("all tables skipped. cannot render query")
|
||||
}
|
||||
|
||||
var ignored uint32
|
||||
@ -161,6 +175,9 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
continue
|
||||
}
|
||||
child := &c.s[cid]
|
||||
if child.SkipRender {
|
||||
continue
|
||||
}
|
||||
|
||||
st.Push(child.ID + closeBlock)
|
||||
st.Push(child.ID)
|
||||
@ -207,7 +224,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
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
|
||||
|
||||
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{}{}
|
||||
}
|
||||
|
||||
for i := range sel.OrderBy {
|
||||
colmap[sel.OrderBy[i].Col] = struct{}{}
|
||||
}
|
||||
|
||||
for _, id := range sel.Children {
|
||||
child := &c.s[id]
|
||||
|
||||
rel, err := c.schema.GetRel(child.Name, ti.Name)
|
||||
if err != nil {
|
||||
skipped |= (1 << uint(id))
|
||||
continue
|
||||
return 0, nil, err
|
||||
//skipped |= (1 << uint(id))
|
||||
//continue
|
||||
}
|
||||
|
||||
switch rel.Type {
|
||||
case RelOneToOne, RelOneToMany:
|
||||
if _, ok := colmap[rel.Right.Col]; !ok {
|
||||
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:
|
||||
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 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{}{}
|
||||
}
|
||||
colmap[rel.Left.Col] = struct{}{}
|
||||
|
||||
case RelRemote:
|
||||
if _, ok := colmap[rel.Left.Col]; !ok {
|
||||
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Left.Col, FieldName: rel.Right.Col})
|
||||
colmap[rel.Left.Col] = struct{}{}
|
||||
skipped |= (1 << uint(id))
|
||||
}
|
||||
colmap[rel.Left.Col] = struct{}{}
|
||||
skipped |= (1 << uint(id))
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
// SELECT
|
||||
@ -267,9 +311,8 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
||||
io.WriteString(c.w, `"`)
|
||||
|
||||
if hasOrder {
|
||||
err := c.renderOrderBy(sel, ti)
|
||||
if err != nil {
|
||||
return skipped, err
|
||||
if err := c.renderOrderBy(sel, ti); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,8 +341,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
||||
|
||||
c.renderRemoteRelColumns(sel, ti)
|
||||
|
||||
err := c.renderJoinedColumns(sel, ti, skipped)
|
||||
if err != nil {
|
||||
if err = c.renderJoinedColumns(sel, ti, skipped); err != nil {
|
||||
return skipped, err
|
||||
}
|
||||
|
||||
@ -318,7 +360,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
||||
// END-SELECT
|
||||
|
||||
// FROM (SELECT .... )
|
||||
err = c.renderBaseSelect(sel, ti, childCols, skipped)
|
||||
err = c.renderBaseSelect(sel, ti, rel, childCols, skipped)
|
||||
if err != nil {
|
||||
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 {
|
||||
colsRendered := len(sel.Cols) != 0
|
||||
|
||||
// columns previously rendered
|
||||
i := len(sel.Cols)
|
||||
|
||||
for _, id := range sel.Children {
|
||||
skipThis := hasBit(skipped, uint32(id))
|
||||
|
||||
if colsRendered && !skipThis {
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
if skipThis {
|
||||
if hasBit(skipped, uint32(id)) {
|
||||
continue
|
||||
}
|
||||
childSel := &c.s[id]
|
||||
if childSel.SkipRender {
|
||||
continue
|
||||
}
|
||||
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
|
||||
//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, childSel.FieldName)
|
||||
io.WriteString(c.w, `"`)
|
||||
i++
|
||||
}
|
||||
|
||||
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 {
|
||||
var groupBy []int
|
||||
|
||||
isRoot := sel.ParentID == -1
|
||||
isRoot := (rel == nil)
|
||||
isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop)
|
||||
isSearch := sel.Args["search"] != nil
|
||||
isAgg := false
|
||||
|
||||
colmap := make(map[string]struct{}, (len(sel.Cols) + len(sel.OrderBy)))
|
||||
|
||||
io.WriteString(c.w, ` FROM (SELECT `)
|
||||
|
||||
i := 0
|
||||
for n, col := range sel.Cols {
|
||||
cn := col.Name
|
||||
colmap[cn] = struct{}{}
|
||||
|
||||
_, 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 {
|
||||
if _, ok := colmap[col.Name]; ok {
|
||||
continue
|
||||
}
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
@ -637,10 +703,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
|
||||
io.WriteString(c.w, ` FROM `)
|
||||
|
||||
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name)
|
||||
io.WriteString(c.w, `"`)
|
||||
io.WriteString(c.w, ti.Name)
|
||||
io.WriteString(c.w, `"`)
|
||||
c.renderFrom(sel, ti, rel)
|
||||
|
||||
// if tn, ok := c.tmap[sel.Name]; ok {
|
||||
// //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 (`)
|
||||
|
||||
if err := c.renderRelationship(sel, ti); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isFil {
|
||||
io.WriteString(c.w, ` AND `)
|
||||
if err := c.renderWhere(sel, ti); err != nil {
|
||||
@ -725,6 +786,44 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderFrom(sel *qcode.Select, ti *DBTableInfo, rel *DBRel) error {
|
||||
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);
|
||||
|
||||
io.WriteString(c.w, `"`)
|
||||
io.WriteString(c.w, rel.Left.Table)
|
||||
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, 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
|
||||
|
||||
@ -807,7 +906,13 @@ func (c *compilerContext) renderRelationshipByName(table, parent string, id int3
|
||||
io.WriteString(c.w, `) = (`)
|
||||
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, `))`)
|
||||
|
||||
return nil
|
||||
@ -1062,27 +1167,27 @@ func (c *compilerContext) renderOrderBy(sel *qcode.Select, ti *DBTableInfo) erro
|
||||
switch ob.Order {
|
||||
case qcode.OrderAsc:
|
||||
//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`)
|
||||
case qcode.OrderDesc:
|
||||
//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`)
|
||||
case qcode.OrderAscNullsFirst:
|
||||
//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`)
|
||||
case qcode.OrderDescNullsFirst:
|
||||
//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`)
|
||||
case qcode.OrderAscNullsLast:
|
||||
//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`)
|
||||
case qcode.OrderDescNullsLast:
|
||||
//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`)
|
||||
default:
|
||||
return fmt.Errorf("13: unexpected value %v", ob.Order)
|
||||
|
@ -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) {
|
||||
gql := `query {
|
||||
user(id: 5, where: { id: { gt: 3 } }) {
|
||||
@ -524,6 +574,8 @@ func TestCompileQuery(t *testing.T) {
|
||||
t.Run("queryWithVariables", queryWithVariables)
|
||||
t.Run("withWhereOnRelations", withWhereOnRelations)
|
||||
t.Run("multiRoot", multiRoot)
|
||||
t.Run("jsonColumnAsTable", jsonColumnAsTable)
|
||||
t.Run("skipUserIDForAnonRole", skipUserIDForAnonRole)
|
||||
t.Run("blockedQuery", blockedQuery)
|
||||
t.Run("blockedFunctions", blockedFunctions)
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ type DBSchema struct {
|
||||
|
||||
type DBTableInfo struct {
|
||||
Name string
|
||||
Type string
|
||||
Singular bool
|
||||
Columns []DBColumn
|
||||
PrimaryCol *DBColumn
|
||||
@ -29,6 +30,7 @@ const (
|
||||
RelOneToOne RelType = iota + 1
|
||||
RelOneToMany
|
||||
RelOneToManyThrough
|
||||
RelEmbedded
|
||||
RelRemote
|
||||
)
|
||||
|
||||
@ -51,7 +53,6 @@ type DBRel struct {
|
||||
}
|
||||
|
||||
func NewDBSchema(info *DBInfo, aliases map[string][]string) (*DBSchema, error) {
|
||||
|
||||
schema := &DBSchema{
|
||||
t: make(map[string]*DBTableInfo),
|
||||
rm: make(map[string]map[string]*DBRel),
|
||||
@ -83,6 +84,7 @@ func (s *DBSchema) addTable(
|
||||
singular := flect.Singularize(t.Key)
|
||||
s.t[singular] = &DBTableInfo{
|
||||
Name: t.Name,
|
||||
Type: t.Type,
|
||||
Singular: true,
|
||||
Columns: cols,
|
||||
ColMap: colmap,
|
||||
@ -92,6 +94,7 @@ func (s *DBSchema) addTable(
|
||||
plural := flect.Pluralize(t.Key)
|
||||
s.t[plural] = &DBTableInfo{
|
||||
Name: t.Name,
|
||||
Type: t.Type,
|
||||
Singular: false,
|
||||
Columns: cols,
|
||||
ColMap: colmap,
|
||||
@ -136,20 +139,46 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
|
||||
return fmt.Errorf("invalid foreign key table '%s'", ct)
|
||||
}
|
||||
|
||||
for _, c := range cols {
|
||||
if len(c.FKeyTable) == 0 || len(c.FKeyColID) == 0 {
|
||||
for i := range cols {
|
||||
c := cols[i]
|
||||
|
||||
if len(c.FKeyTable) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Foreign key column name
|
||||
ft := strings.ToLower(c.FKeyTable)
|
||||
fcid := c.FKeyColID[0]
|
||||
|
||||
ti, ok := s.t[ft]
|
||||
if !ok {
|
||||
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]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid foreign key column id '%d' for table '%s'",
|
||||
@ -188,10 +217,12 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
|
||||
rel2 = &DBRel{Type: RelOneToMany}
|
||||
}
|
||||
|
||||
rel2.Left.col = fc
|
||||
rel2.Left.Table = c.FKeyTable
|
||||
rel2.Left.Col = fc.Name
|
||||
rel2.Left.Array = fc.Array
|
||||
|
||||
rel2.Right.col = &c
|
||||
rel2.Right.Table = t.Name
|
||||
rel2.Right.Col = c.Name
|
||||
rel2.Right.Array = c.Array
|
||||
@ -249,9 +280,11 @@ func (s *DBSchema) updateSchemaOTMT(
|
||||
rel1.Through = ti.Name
|
||||
rel1.ColT = col2.Name
|
||||
|
||||
rel1.Left.col = &col2
|
||||
rel1.Left.Table = col2.FKeyTable
|
||||
rel1.Left.Col = fc2.Name
|
||||
|
||||
rel1.Right.col = &col1
|
||||
rel1.Right.Table = ti.Name
|
||||
rel1.Right.Col = col1.Name
|
||||
|
||||
@ -265,9 +298,11 @@ func (s *DBSchema) updateSchemaOTMT(
|
||||
rel2.Through = ti.Name
|
||||
rel2.ColT = col1.Name
|
||||
|
||||
rel1.Left.col = fc1
|
||||
rel2.Left.Table = col1.FKeyTable
|
||||
rel2.Left.Col = fc1.Name
|
||||
|
||||
rel1.Right.col = &col2
|
||||
rel2.Right.Table = ti.Name
|
||||
rel2.Right.Col = col2.Name
|
||||
|
||||
|
@ -12,6 +12,8 @@ func (rt RelType) String() string {
|
||||
return "one to many through"
|
||||
case RelRemote:
|
||||
return "remote"
|
||||
case RelEmbedded:
|
||||
return "embedded"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -62,6 +62,20 @@ func GetDBInfo(db *pgxpool.Pool) (*DBInfo, error) {
|
||||
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) {
|
||||
v, ok := di.colmap[strings.ToLower(table)][strings.ToLower(column)]
|
||||
return v, ok
|
||||
|
@ -125,16 +125,16 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re
|
||||
if item.relPC.Type == RelOneToMany {
|
||||
if conn, ok := item.data["where"]; ok {
|
||||
io.WriteString(w, ` AND `)
|
||||
renderWhereFromJSON(w, item.ti.Name, conn)
|
||||
renderWhereFromJSON(w, item.kvitem, "where", conn)
|
||||
} else if conn, ok := item.data["_where"]; ok {
|
||||
io.WriteString(w, ` AND `)
|
||||
renderWhereFromJSON(w, item.ti.Name, conn)
|
||||
renderWhereFromJSON(w, item.kvitem, "_where", conn)
|
||||
}
|
||||
}
|
||||
io.WriteString(w, `)`)
|
||||
|
||||
} else {
|
||||
io.WriteString(w, `WHERE `)
|
||||
io.WriteString(w, ` WHERE `)
|
||||
if err := c.renderWhere(&qc.Selects[0], ti); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -167,7 +167,15 @@ func renderNestedUpdateRelColumns(w io.Writer, item kvitem, values bool) error {
|
||||
if values {
|
||||
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col)
|
||||
} else {
|
||||
quoted(w, v.relCP.Right.Col)
|
||||
if v.relCP.Right.Array {
|
||||
io.WriteString(w, `array_remove(`)
|
||||
colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col)
|
||||
io.WriteString(w, `, `)
|
||||
quoted(w, v.relCP.Right.Col)
|
||||
io.WriteString(w, `)`)
|
||||
} else {
|
||||
quoted(w, v.relCP.Right.Col)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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{
|
||||
"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{
|
||||
"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{
|
||||
"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{
|
||||
"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{
|
||||
"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{
|
||||
"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), "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", "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), "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), "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", "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"`
|
||||
|
||||
vars := map[string]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), "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(`{
|
||||
@ -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) {
|
||||
t.Run("singleUpdate", singleUpdate)
|
||||
t.Run("simpleUpdateWithPresets", simpleUpdateWithPresets)
|
||||
@ -304,4 +335,5 @@ func TestCompileUpdate(t *testing.T) {
|
||||
t.Run("nestedUpdateOneToManyWithConnect", nestedUpdateOneToManyWithConnect)
|
||||
t.Run("nestedUpdateOneToOneWithConnect", nestedUpdateOneToOneWithConnect)
|
||||
t.Run("nestedUpdateOneToOneWithDisconnect", nestedUpdateOneToOneWithDisconnect)
|
||||
//t.Run("nestedUpdateOneToOneWithDisconnectArray", nestedUpdateOneToOneWithDisconnectArray)
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ type trval struct {
|
||||
query struct {
|
||||
limit string
|
||||
fil *Exp
|
||||
filNU bool
|
||||
cols map[string]struct{}
|
||||
disable struct {
|
||||
funcs bool
|
||||
@ -53,6 +54,7 @@ type trval struct {
|
||||
|
||||
insert struct {
|
||||
fil *Exp
|
||||
filNU bool
|
||||
cols map[string]struct{}
|
||||
psmap map[string]string
|
||||
pslist []string
|
||||
@ -60,14 +62,16 @@ type trval struct {
|
||||
|
||||
update struct {
|
||||
fil *Exp
|
||||
filNU bool
|
||||
cols map[string]struct{}
|
||||
psmap map[string]string
|
||||
pslist []string
|
||||
}
|
||||
|
||||
delete struct {
|
||||
fil *Exp
|
||||
cols map[string]struct{}
|
||||
fil *Exp
|
||||
filNU bool
|
||||
cols map[string]struct{}
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,21 +92,21 @@ func (trv *trval) allowedColumns(qt QType) map[string]struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (trv *trval) filter(qt QType) *Exp {
|
||||
func (trv *trval) filter(qt QType) (*Exp, bool) {
|
||||
switch qt {
|
||||
case QTQuery:
|
||||
return trv.query.fil
|
||||
return trv.query.fil, trv.query.filNU
|
||||
case QTInsert:
|
||||
return trv.insert.fil
|
||||
return trv.insert.fil, trv.insert.filNU
|
||||
case QTUpdate:
|
||||
return trv.update.fil
|
||||
return trv.update.fil, trv.update.filNU
|
||||
case QTDelete:
|
||||
return trv.delete.fil
|
||||
return trv.delete.fil, trv.delete.filNU
|
||||
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{} {
|
||||
|
@ -167,11 +167,6 @@ func parseSelectionSet(gql []byte) (*Operation, error) {
|
||||
return nil, fmt.Errorf("invalid '%s' found after closing '}'", p.current())
|
||||
}
|
||||
|
||||
// for i := p.pos; i < len(p.items); i++ {
|
||||
// fmt.Printf("2>>>> %#v\n", p.items[i])
|
||||
// }
|
||||
//return nil, fmt.Errorf("unexpected token")
|
||||
|
||||
lexPool.Put(l)
|
||||
|
||||
return op, err
|
||||
|
@ -51,6 +51,7 @@ type Select struct {
|
||||
Allowed map[string]struct{}
|
||||
PresetMap map[string]string
|
||||
PresetList []string
|
||||
SkipRender bool
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
@ -187,7 +188,7 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
||||
trv := &trval{}
|
||||
|
||||
// query config
|
||||
trv.query.fil, err = compileFilter(trc.Query.Filters)
|
||||
trv.query.fil, trv.query.filNU, err = compileFilter(trc.Query.Filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -198,7 +199,8 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
||||
trv.query.disable.funcs = trc.Query.DisableFunctions
|
||||
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
err := com.compileArgs(qc, s, field.Args)
|
||||
err := com.compileArgs(qc, s, field.Args, role)
|
||||
if err != nil {
|
||||
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) {
|
||||
var fil *Exp
|
||||
var nu bool
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -399,6 +410,10 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
||||
return
|
||||
}
|
||||
|
||||
if nu && role == "anon" {
|
||||
sel.SkipRender = true
|
||||
}
|
||||
|
||||
switch fil.Op {
|
||||
case OpNop:
|
||||
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 ka bool
|
||||
|
||||
@ -435,7 +450,7 @@ func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg) error {
|
||||
err, ka = com.compileArgSearch(sel, arg)
|
||||
|
||||
case "where":
|
||||
err, ka = com.compileArgWhere(sel, arg)
|
||||
err, ka = com.compileArgWhere(sel, arg, role)
|
||||
|
||||
case "orderby", "order_by", "order":
|
||||
err, ka = com.compileArgOrderBy(sel, arg)
|
||||
@ -501,19 +516,20 @@ func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
|
||||
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 {
|
||||
return nil, fmt.Errorf("expecting an object")
|
||||
return nil, false, fmt.Errorf("expecting an object")
|
||||
}
|
||||
|
||||
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 needsUser bool
|
||||
|
||||
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)
|
||||
@ -526,7 +542,7 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
||||
intf := st.Pop()
|
||||
node, ok := intf.(*Node)
|
||||
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
|
||||
@ -542,13 +558,17 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
||||
|
||||
ex, err := newExp(st, node, usePool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, needsUser, err
|
||||
}
|
||||
|
||||
if ex == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if ex.Type == ValVar && ex.Val == "user_id" {
|
||||
needsUser = true
|
||||
}
|
||||
|
||||
if node.exp == nil {
|
||||
root = ex
|
||||
} else {
|
||||
@ -571,7 +591,7 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
||||
nodePool.Put(node)
|
||||
}
|
||||
|
||||
return root, nil
|
||||
return root, needsUser, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
var err error
|
||||
|
||||
ex, err := com.compileArgObj(st, arg)
|
||||
ex, nu, err := com.compileArgObj(st, arg)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
if nu && role == "anon" {
|
||||
sel.SkipRender = true
|
||||
}
|
||||
|
||||
if sel.Where != nil {
|
||||
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 needsUser bool
|
||||
|
||||
com := &Compiler{}
|
||||
st := util.NewStack()
|
||||
|
||||
if len(filter) == 0 {
|
||||
return &Exp{Op: OpNop, doFree: false}, nil
|
||||
return &Exp{Op: OpNop, doFree: false}, false, nil
|
||||
}
|
||||
|
||||
for i := range filter {
|
||||
if filter[i] == "false" {
|
||||
return &Exp{Op: OpFalse, doFree: false}, nil
|
||||
return &Exp{Op: OpFalse, doFree: false}, false, nil
|
||||
}
|
||||
|
||||
node, err := ParseArgValue(filter[i])
|
||||
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 {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
if nu {
|
||||
needsUser = true
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
}
|
||||
return fl, nil
|
||||
return fl, needsUser, nil
|
||||
}
|
||||
|
||||
func buildPath(a []string) string {
|
||||
|
@ -128,10 +128,10 @@ func (al *allowList) upsert(query, vars []byte, uri string) {
|
||||
|
||||
var key string
|
||||
|
||||
if len(name) == 0 {
|
||||
key = hash
|
||||
} else {
|
||||
if len(name) != 0 {
|
||||
key = name
|
||||
} else {
|
||||
key = hash
|
||||
}
|
||||
|
||||
if i, ok := al.index[key]; !ok {
|
||||
@ -280,7 +280,7 @@ func (al *allowList) save(item *allowItem) {
|
||||
|
||||
for i := range v {
|
||||
if len(v[i].vars) != 0 && !bytes.Equal(v[i].vars, []byte("{}")) {
|
||||
vj, err := json.MarshalIndent(v[i].vars, "", "\t")
|
||||
vj, err := json.MarshalIndent(v[i].vars, "", " ")
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Failed to write allow list 'vars' to file")
|
||||
continue
|
||||
|
@ -324,7 +324,7 @@ Branch : %v
|
||||
Go version : %v
|
||||
|
||||
Licensed under the Apache Public License 2.0
|
||||
Copyright 2015-2019 Vikram Rangnekar.
|
||||
Copyright 2020, Vikram Rangnekar.
|
||||
`,
|
||||
version,
|
||||
lastCommitSHA,
|
||||
|
@ -63,7 +63,7 @@ func cmdDBCreate(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
@ -83,7 +83,7 @@ func cmdDBDrop(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
|
@ -8,17 +8,21 @@ func cmdServ(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to connect to database")
|
||||
if conf != nil {
|
||||
db, err = initDBPool(conf)
|
||||
|
||||
if err == nil {
|
||||
initCompiler()
|
||||
initAllowList(confPath)
|
||||
initPreparedList()
|
||||
} else {
|
||||
fatalInProd(err, "failed to connect to database")
|
||||
}
|
||||
}
|
||||
|
||||
initCompiler()
|
||||
initAllowList(confPath)
|
||||
initPreparedList()
|
||||
initWatcher(confPath)
|
||||
|
||||
startHTTP()
|
||||
|
@ -87,6 +87,7 @@ type config struct {
|
||||
|
||||
type configColumn struct {
|
||||
Name string
|
||||
Type string
|
||||
ForeignKey string `mapstructure:"related_to"`
|
||||
}
|
||||
|
||||
@ -313,7 +314,7 @@ func (c *config) getAliasMap() map[string][]string {
|
||||
for i := range c.Tables {
|
||||
t := c.Tables[i]
|
||||
|
||||
if len(t.Table) == 0 {
|
||||
if len(t.Table) == 0 || len(t.Columns) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -8,9 +8,61 @@ import (
|
||||
"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 {
|
||||
for _, t := range c.Tables {
|
||||
for _, c := range t.Columns {
|
||||
if len(c.ForeignKey) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := addForeignKey(di, c, t); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -23,7 +75,7 @@ func addForeignKey(di *psql.DBInfo, c configColumn, t configTable) error {
|
||||
c1, ok := di.GetColumn(t.Name, c.Name)
|
||||
if !ok {
|
||||
return fmt.Errorf(
|
||||
"Invalid table '%s' or column '%s in config",
|
||||
"Invalid table '%s' or column '%s' in config",
|
||||
t.Name, c.Name)
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
|
||||
// Ensure that we use the correct events, as they are not uniform across
|
||||
// platforms. See https://github.com/fsnotify/fsnotify/issues/74
|
||||
|
||||
if !conf.Production && strings.HasSuffix(event.Name, "/allow.list") {
|
||||
if conf != nil && !conf.Production && strings.HasSuffix(event.Name, "/allow.list") {
|
||||
continue
|
||||
}
|
||||
|
||||
|
56
serv/serv.go
56
serv/serv.go
@ -21,6 +21,10 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err = addTables(c, di); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err = addForeignKeys(c, di); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -50,7 +54,7 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
||||
}
|
||||
|
||||
func initWatcher(cpath string) {
|
||||
if !conf.WatchAndReload {
|
||||
if conf != nil && !conf.WatchAndReload {
|
||||
return
|
||||
}
|
||||
|
||||
@ -70,18 +74,33 @@ func initWatcher(cpath string) {
|
||||
}
|
||||
|
||||
func startHTTP() {
|
||||
hp := strings.SplitN(conf.HostPort, ":", 2)
|
||||
var hostPort string
|
||||
var appName string
|
||||
|
||||
if len(conf.Host) != 0 {
|
||||
hp[0] = conf.Host
|
||||
defaultHP := "0.0.0.0:8080"
|
||||
env := os.Getenv("GO_ENV")
|
||||
|
||||
if conf != nil {
|
||||
appName = conf.AppName
|
||||
hp := strings.SplitN(conf.HostPort, ":", 2)
|
||||
|
||||
if len(hp) == 2 {
|
||||
if len(conf.Host) != 0 {
|
||||
hp[0] = conf.Host
|
||||
}
|
||||
|
||||
if len(conf.Port) != 0 {
|
||||
hp[1] = conf.Port
|
||||
}
|
||||
|
||||
hostPort = fmt.Sprintf("%s:%s", hp[0], hp[1])
|
||||
}
|
||||
}
|
||||
|
||||
if len(conf.Port) != 0 {
|
||||
hp[1] = conf.Port
|
||||
if len(hostPort) == 0 {
|
||||
hostPort = defaultHP
|
||||
}
|
||||
|
||||
hostPort := fmt.Sprintf("%s:%s", hp[0], hp[1])
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: hostPort,
|
||||
Handler: routeHandler(),
|
||||
@ -110,8 +129,8 @@ func startHTTP() {
|
||||
Str("version", version).
|
||||
Str("git_branch", gitBranch).
|
||||
Str("host_post", hostPort).
|
||||
Str("app_name", conf.AppName).
|
||||
Str("env", conf.Env).
|
||||
Str("app_name", appName).
|
||||
Str("env", env).
|
||||
Msgf("%s listening", serverName)
|
||||
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
@ -124,7 +143,7 @@ func startHTTP() {
|
||||
func routeHandler() http.Handler {
|
||||
var apiH http.Handler
|
||||
|
||||
if conf.HTTPGZip {
|
||||
if conf != nil && conf.HTTPGZip {
|
||||
gzipH := gziphandler.MustNewGzipLevelHandler(6)
|
||||
apiH = gzipH(http.HandlerFunc(apiV1))
|
||||
} else {
|
||||
@ -132,11 +151,14 @@ func routeHandler() http.Handler {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", health)
|
||||
mux.Handle("/api/v1/graphql", withAuth(apiH))
|
||||
|
||||
if conf.WebUI {
|
||||
mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox()))
|
||||
if conf != nil {
|
||||
mux.HandleFunc("/health", health)
|
||||
mux.Handle("/api/v1/graphql", withAuth(apiH))
|
||||
|
||||
if conf.WebUI {
|
||||
mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox()))
|
||||
}
|
||||
}
|
||||
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -170,3 +192,7 @@ func getConfigName() string {
|
||||
|
||||
return ge
|
||||
}
|
||||
|
||||
func isDev() bool {
|
||||
return strings.HasPrefix(os.Getenv("GO_ENV"), "dev")
|
||||
}
|
||||
|
@ -141,3 +141,11 @@ func findStmt(role string, stmts []stmt) *stmt {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fatalInProd(err error, msg string) {
|
||||
if isDev() {
|
||||
errlog.Error().Err(err).Msg(msg)
|
||||
} else {
|
||||
errlog.Fatal().Err(err).Msg(msg)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user