Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
8b6c562ac1 | |||
a1fb89b762 | |||
c82a7bff0d | |||
7acf28bb3c | |||
be5d4e976a | |||
d1b884aec6 | |||
4be4ce860b | |||
dfa4caf540 | |||
7763251fb7 | |||
51e105699e | |||
90694f8803 | |||
ad82f5b267 | |||
99b37a9c50 | |||
7ec1f59224 | |||
d3ecb1d6cc | |||
aed4170e8e | |||
c33e93ab37 | |||
3d3e5d9c2b | |||
67b4a4d945 | |||
7413813138 | |||
12007db76e | |||
c85d379fe2 | |||
62fd1eac55 |
18
Dockerfile
18
Dockerfile
@ -6,14 +6,19 @@ RUN yarn
|
||||
RUN yarn build
|
||||
|
||||
# stage: 2
|
||||
FROM golang:1.13.4-alpine as go-build
|
||||
FROM golang:1.14-alpine as go-build
|
||||
RUN apk update && \
|
||||
apk add --no-cache make && \
|
||||
apk add --no-cache git && \
|
||||
apk add --no-cache jq && \
|
||||
apk add --no-cache upx=3.95-r2
|
||||
|
||||
RUN GO111MODULE=off go get -u github.com/rafaelsq/wtc
|
||||
|
||||
ARG SOPS_VERSION=3.5.0
|
||||
ADD https://github.com/mozilla/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux /usr/local/bin/sops
|
||||
RUN chmod 755 /usr/local/bin/sops
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
|
||||
@ -22,9 +27,9 @@ COPY --from=react-build /web/build/ ./web/build/
|
||||
|
||||
RUN go mod vendor
|
||||
RUN make build
|
||||
RUN echo "Compressing binary, will take a bit of time..." && \
|
||||
upx --ultra-brute -qq super-graph && \
|
||||
upx -t super-graph
|
||||
# RUN echo "Compressing binary, will take a bit of time..." && \
|
||||
# upx --ultra-brute -qq super-graph && \
|
||||
# upx -t super-graph
|
||||
|
||||
# stage: 3
|
||||
FROM alpine:latest
|
||||
@ -36,10 +41,15 @@ RUN mkdir -p /config
|
||||
COPY --from=go-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=go-build /app/config/* /config/
|
||||
COPY --from=go-build /app/super-graph .
|
||||
COPY --from=go-build /app/scripts/start.sh .
|
||||
COPY --from=go-build /usr/local/bin/sops .
|
||||
|
||||
RUN chmod +x /super-graph
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
USER nobody
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ./super-graph serv
|
||||
ENTRYPOINT ["./start.sh"]
|
||||
|
4
Makefile
4
Makefile
@ -35,7 +35,7 @@ $(GORICE):
|
||||
|
||||
$(WEB_BUILD_DIR):
|
||||
@echo "First install Yarn and create a build of the web UI found under ./web"
|
||||
@echo "Command: cd web && yarn build"
|
||||
@echo "Command: cd web && yarn && yarn build"
|
||||
@exit 1
|
||||
|
||||
$(GITCHGLOG):
|
||||
@ -77,7 +77,7 @@ clean:
|
||||
run: clean
|
||||
@go run $(BUILD_FLAGS) main.go $(ARGS)
|
||||
|
||||
install:
|
||||
install: gen
|
||||
@echo $(GOPATH)
|
||||
@echo "Commit Hash: `git rev-parse HEAD`"
|
||||
@echo "Old Hash: `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`"
|
||||
|
@ -48,6 +48,7 @@ This compiler is what sits at the heart of Super Graph with layers of useful fun
|
||||
- Fuzz tested for security
|
||||
- Database migrations tool
|
||||
- Database seeding tool
|
||||
- Works with Postgres and YugabyteDB
|
||||
|
||||
## Get started
|
||||
|
||||
|
@ -20,9 +20,9 @@ const (
|
||||
type Item struct {
|
||||
Name string
|
||||
key string
|
||||
URI string
|
||||
Query string
|
||||
Vars json.RawMessage
|
||||
Comment string
|
||||
}
|
||||
|
||||
type List struct {
|
||||
@ -105,7 +105,7 @@ func (al *List) IsPersist() bool {
|
||||
return al.saveChan != nil
|
||||
}
|
||||
|
||||
func (al *List) Add(vars []byte, query, uri string) error {
|
||||
func (al *List) Set(vars []byte, query, comment string) error {
|
||||
if al.saveChan == nil {
|
||||
return errors.New("allow.list is read-only")
|
||||
}
|
||||
@ -129,7 +129,7 @@ func (al *List) Add(vars []byte, query, uri string) error {
|
||||
}
|
||||
|
||||
al.saveChan <- Item{
|
||||
URI: uri,
|
||||
Comment: comment,
|
||||
Query: q,
|
||||
Vars: vars,
|
||||
}
|
||||
@ -149,7 +149,7 @@ func (al *List) Load() ([]Item, error) {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
var uri string
|
||||
var comment bytes.Buffer
|
||||
var varBytes []byte
|
||||
|
||||
itemMap := make(map[string]struct{})
|
||||
@ -166,7 +166,7 @@ func (al *List) Load() ([]Item, error) {
|
||||
e++
|
||||
}
|
||||
if (e - s) > 2 {
|
||||
uri = strings.TrimSpace(string(b[(s + 1):e]))
|
||||
comment.Write(b[(s + 1):(e + 1)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,11 +209,12 @@ func (al *List) Load() ([]Item, error) {
|
||||
v := Item{
|
||||
Name: name,
|
||||
key: key,
|
||||
URI: uri,
|
||||
Query: query,
|
||||
Vars: varBytes,
|
||||
Comment: comment.String(),
|
||||
}
|
||||
list = append(list, v)
|
||||
comment.Reset()
|
||||
}
|
||||
|
||||
varBytes = nil
|
||||
@ -252,6 +253,9 @@ func (al *List) save(item Item) error {
|
||||
}
|
||||
|
||||
if index != -1 {
|
||||
if len(list[index].Comment) != 0 {
|
||||
item.Comment = list[index].Comment
|
||||
}
|
||||
list[index] = item
|
||||
} else {
|
||||
list = append(list, item)
|
||||
@ -269,10 +273,30 @@ func (al *List) save(item Item) error {
|
||||
})
|
||||
|
||||
for _, v := range list {
|
||||
_, err := f.WriteString(fmt.Sprintf("# %s\n\n", v.URI))
|
||||
cmtLines := strings.Split(v.Comment, "\n")
|
||||
|
||||
i := 0
|
||||
for _, c := range cmtLines {
|
||||
if c = strings.TrimSpace(c); len(c) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := f.WriteString(fmt.Sprintf("# %s\n", c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if i != 0 {
|
||||
if _, err := f.WriteString("\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := f.WriteString(fmt.Sprintf("# Query named %s\n\n", v.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.Vars) != 0 && !bytes.Equal(v.Vars, []byte("{}")) {
|
||||
vj, err := json.MarshalIndent(v.Vars, "", " ")
|
||||
@ -317,17 +341,13 @@ func QueryName(b string) string {
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
switch {
|
||||
case state == 2 && b[i] == '{':
|
||||
return b[s:i]
|
||||
case state == 2 && b[i] == ' ':
|
||||
case state == 2 && !isValidNameChar(b[i]):
|
||||
return b[s:i]
|
||||
case state == 1 && b[i] == '{':
|
||||
return ""
|
||||
case state == 1 && b[i] != ' ':
|
||||
case state == 1 && isValidNameChar(b[i]):
|
||||
s = i
|
||||
state = 2
|
||||
case state == 1 && b[i] == ' ':
|
||||
continue
|
||||
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
|
||||
state = 1
|
||||
}
|
||||
@ -335,3 +355,7 @@ func QueryName(b string) string {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func isValidNameChar(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
|
||||
}
|
||||
|
@ -21,7 +21,9 @@ func TestGQLName1(t *testing.T) {
|
||||
|
||||
func TestGQLName2(t *testing.T) {
|
||||
var q = `
|
||||
query hakuna_matata {
|
||||
query hakuna_matata
|
||||
|
||||
{
|
||||
products(
|
||||
distinct: [price]
|
||||
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
|
||||
|
@ -32,6 +32,10 @@ reload_on_config_change: true
|
||||
# Path pointing to where the migrations can be found
|
||||
migrations_path: ./config/migrations
|
||||
|
||||
# Secret key for general encryption operations like
|
||||
# encrypting the cursor data
|
||||
secret_key: supercalifajalistics
|
||||
|
||||
# Postgres related environment Variables
|
||||
# SG_DATABASE_HOST
|
||||
# SG_DATABASE_PORT
|
||||
@ -89,7 +93,7 @@ database:
|
||||
port: 5432
|
||||
dbname: app_development
|
||||
user: postgres
|
||||
password: ''
|
||||
password: postgres
|
||||
|
||||
#schema: "public"
|
||||
#pool_size: 10
|
||||
@ -167,10 +171,13 @@ roles:
|
||||
block: false
|
||||
|
||||
- name: deals
|
||||
|
||||
query:
|
||||
limit: 3
|
||||
columns: ["name", "description" ]
|
||||
aggregation: false
|
||||
|
||||
- name: purchases
|
||||
query:
|
||||
limit: 3
|
||||
aggregation: false
|
||||
|
||||
- name: user
|
||||
@ -183,12 +190,10 @@ roles:
|
||||
query:
|
||||
limit: 50
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns: ["id", "name", "description", "search_rank", "search_headline_description" ]
|
||||
disable_functions: false
|
||||
|
||||
insert:
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns: ["id", "name", "description" ]
|
||||
presets:
|
||||
- user_id: "$user_id"
|
||||
- created_at: "now"
|
||||
|
@ -32,6 +32,10 @@ enable_tracing: true
|
||||
# Path pointing to where the migrations can be found
|
||||
# migrations_path: migrations
|
||||
|
||||
# Secret key for general encryption operations like
|
||||
# encrypting the cursor data
|
||||
# secret_key: supercalifajalistics
|
||||
|
||||
# Postgres related environment Variables
|
||||
# SG_DATABASE_HOST
|
||||
# SG_DATABASE_PORT
|
||||
@ -50,7 +54,7 @@ database:
|
||||
port: 5432
|
||||
dbname: app_production
|
||||
user: postgres
|
||||
password: ''
|
||||
password: postgres
|
||||
#pool_size: 10
|
||||
#max_retries: 0
|
||||
#log_level: "debug"
|
||||
|
@ -11,7 +11,7 @@ for (i = 0; i < user_count; i++) {
|
||||
var pwd = fake.password()
|
||||
var data = {
|
||||
full_name: fake.name(),
|
||||
avatar: fake.image_url(),
|
||||
avatar: fake.avatar_url(200),
|
||||
phone: fake.phone(),
|
||||
email: fake.email(),
|
||||
password: pwd,
|
||||
|
80
crypto/encrypt.go
Normal file
80
crypto/encrypt.go
Normal file
@ -0,0 +1,80 @@
|
||||
// cryptopasta - basic cryptography examples
|
||||
//
|
||||
// Written in 2015 by George Tankersley <george.tankersley@gmail.com>
|
||||
//
|
||||
// To the extent possible under law, the author(s) have dedicated all copyright
|
||||
// and related and neighboring rights to this software to the public domain
|
||||
// worldwide. This software is distributed without any warranty.
|
||||
//
|
||||
// You should have received a copy of the CC0 Public Domain Dedication along
|
||||
// with this software. If not, see // <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
|
||||
// Provides symmetric authenticated encryption using 256-bit AES-GCM with a random nonce.
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// NewEncryptionKey generates a random 256-bit key for Encrypt() and
|
||||
// Decrypt(). It panics if the source of randomness fails.
|
||||
func NewEncryptionKey() [32]byte {
|
||||
key := [32]byte{}
|
||||
_, err := io.ReadFull(rand.Reader, key[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of
|
||||
// the data and provides a check that it hasn't been altered. Output takes the
|
||||
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
||||
func Encrypt(plaintext []byte, key *[32]byte) (ciphertext []byte, err error) {
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
_, err = io.ReadFull(rand.Reader, nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of
|
||||
// the data and provides a check that it hasn't been altered. Expects input
|
||||
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
||||
func Decrypt(ciphertext []byte, key *[32]byte) (plaintext []byte, err error) {
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ciphertext) < gcm.NonceSize() {
|
||||
return nil, errors.New("malformed ciphertext")
|
||||
}
|
||||
|
||||
return gcm.Open(nil,
|
||||
ciphertext[:gcm.NonceSize()],
|
||||
ciphertext[gcm.NonceSize():],
|
||||
nil,
|
||||
)
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
version: '3.4'
|
||||
services:
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:12
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
|
274
docs/guide.md
274
docs/guide.md
@ -34,6 +34,12 @@ Super Graph has a rich feature set like integrating with your existing Ruby on R
|
||||
# clone the repository
|
||||
git clone https://github.com/dosco/super-graph
|
||||
|
||||
# run db in background
|
||||
docker-compose up -d db
|
||||
|
||||
# see logs and wait until DB is really UP
|
||||
docker-compose logs db
|
||||
|
||||
# setup the demo rails app & database and run it
|
||||
docker-compose run rails_app rake db:create db:migrate db:seed
|
||||
|
||||
@ -137,7 +143,7 @@ What if I told you Super Graph will fetch all this data with a single SQL query
|
||||
|
||||
```graphql
|
||||
query {
|
||||
products(limit 5, where: { price: { gt: 12 } }) {
|
||||
products(limit: 5, where: { price: { gt: 12 } }) {
|
||||
id
|
||||
name
|
||||
description
|
||||
@ -153,7 +159,7 @@ query {
|
||||
}
|
||||
}
|
||||
purchases(
|
||||
limit 10,
|
||||
limit: 10,
|
||||
order_by: { created_at: desc } ,
|
||||
where: { user_id: { eq: $user_id } }
|
||||
) {
|
||||
@ -216,7 +222,7 @@ You can then add your database schema to the migrations, maybe create some seed
|
||||
git clone https://github.com/dosco/super-graph && cd super-graph && make install
|
||||
```
|
||||
|
||||
And then create and launch you're new app
|
||||
And then create and launch your new app
|
||||
|
||||
```bash
|
||||
# create a new app and change to it's directory
|
||||
@ -286,6 +292,12 @@ for (i = 0; i < 10; i++) {
|
||||
}
|
||||
```
|
||||
|
||||
If you want to import a lot of data using a CSV file is the best and fastest option. The `import_csv` command uses the `COPY FROM` Postgres method to load massive amounts of data into tables. The first line of the CSV file must be the header with column names.
|
||||
|
||||
```javascript
|
||||
var post_count = import_csv("posts", "posts.csv")
|
||||
```
|
||||
|
||||
You can generate the following fake data for your seeding purposes. Below is the list of fake data functions supported by the built-in fake data library. For example `fake.image_url()` will generate a fake image url or `fake.shuffle_strings(['hello', 'world', 'cool'])` will generate a randomly shuffled version of that array of strings or `fake.rand_string(['hello', 'world', 'cool'])` will return a random string from the array provided.
|
||||
|
||||
```
|
||||
@ -1024,9 +1036,116 @@ mutation {
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
This is a must have feature of any API. When you want your users to go thought a list page by page or implement some fancy infinite scroll you're going to need pagination. There are two ways to paginate in Super Graph.
|
||||
|
||||
Limit-Offset
|
||||
This is simple enough but also inefficient when working with a large number of total items. Limit, limits the number of items fetched and offset is the point you want to fetch from. The below query will fetch 10 results at a time starting with the 100th item. You will have to keep updating offset (110, 120, 130, etc ) to walk thought the results so make offset a variable.
|
||||
|
||||
```graphql
|
||||
query {
|
||||
products(limit: 10, offset: 100) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Cursor
|
||||
This is a powerful and highly efficient way to paginate though a large number of results. Infact it does not matter how many total results there are this will always be lighting fast. You can use a cursor to walk forward of backward though the results. If you plan to implement infinite scroll this is the option you should choose.
|
||||
|
||||
When going this route the results will contain a cursor value this is an encrypted string that you don't have to worry about just pass this back in to the next API call and you'll received the next set of results. The cursor value is encrypted since its contents should only matter to Super Graph and not the client. Also since the primary key is used for this feature it's possible you might not want to leak it's value to clients.
|
||||
|
||||
You will need to set this config value to ensure the encrypted cursor data is secure. If not set a random value is used which will change with each deployment breaking older cursor values that clients might be using so best to set it.
|
||||
|
||||
```yaml
|
||||
# Secret key for general encryption operations like
|
||||
# encrypting the cursor data
|
||||
secret_key: supercalifajalistics
|
||||
```
|
||||
|
||||
Paginating forward through your results
|
||||
|
||||
```json
|
||||
{
|
||||
"variables": {
|
||||
"cursor": "MJoTLbQF4l0GuoDsYmCrpjPeaaIlNpfm4uFU4PQ="
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```graphql
|
||||
query {
|
||||
products(first: 10, after: $cursor) {
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Paginating backward through your results
|
||||
|
||||
```graphql
|
||||
query {
|
||||
products(last: 10, before: $cursor) {
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```graphql
|
||||
"data": {
|
||||
"products": [
|
||||
{
|
||||
"slug": "eius-nulla-et-8",
|
||||
"name" "Pale Ale"
|
||||
},
|
||||
{
|
||||
"slug": "sapiente-ut-alias-12",
|
||||
"name" "Brown Ale"
|
||||
}
|
||||
...
|
||||
],
|
||||
"products_cursor": "dJwHassm5+d82rGydH2xQnwNxJ1dcj4/cxkh5Cer"
|
||||
}
|
||||
```
|
||||
|
||||
Nested tables can also have cursors. Requesting multiple cursors are supported on a single request but when paginating using a cursor only one table is currently supported. To explain this better, you can only use a `before` or `after` argument with a cursor value to paginate a single table in a query.
|
||||
|
||||
```graphql
|
||||
query {
|
||||
products(last: 10) {
|
||||
slug
|
||||
name
|
||||
customers(last: 5) {
|
||||
email
|
||||
full_name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Multiple order-by arguments are supported. Super Graph is smart enough to allow cursor based pagination when you also need complex sort order like below.
|
||||
|
||||
```graphql
|
||||
query {
|
||||
products(
|
||||
last: 10
|
||||
before: $cursor
|
||||
order_by: [ price: desc, total_customers: asc ]) {
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Using Variables
|
||||
|
||||
Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The build-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner
|
||||
Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The built-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.
|
||||
|
||||
```javascript
|
||||
@ -1319,7 +1438,7 @@ auth:
|
||||
max_active: 12000
|
||||
```
|
||||
|
||||
### JWT Token Auth
|
||||
### JWT Tokens
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
@ -1339,6 +1458,51 @@ We can get the JWT token either from the `authorization` header where we expect
|
||||
|
||||
For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
|
||||
|
||||
### HTTP Headers
|
||||
|
||||
```yaml
|
||||
header:
|
||||
name: X-AppEngine-QueueName
|
||||
exists: true
|
||||
#value: default
|
||||
```
|
||||
|
||||
Header auth is usually the best option to authenticate requests to the action endpoints. For example you
|
||||
might want to use an action to refresh a materalized view every hour and only want a cron service like the Google AppEngine Cron service to make that request in this case a config similar to the one above will do.
|
||||
|
||||
The `exists: true` parameter ensures that only the existance of the header is checked not its value. The `value` parameter lets you confirm that the value matches the one assgined to the parameter. This helps in the case you are using a shared secret to protect the endpoint.
|
||||
|
||||
### Named Auth
|
||||
|
||||
```yaml
|
||||
# You can add additional named auths to use with actions
|
||||
# In this example actions using this auth can only be
|
||||
# called from the Google Appengine Cron service that
|
||||
# sets a special header to all it's requests
|
||||
auths:
|
||||
- name: from_taskqueue
|
||||
type: header
|
||||
header:
|
||||
name: X-Appengine-Cron
|
||||
exists: true
|
||||
```
|
||||
|
||||
In addition to the default auth configuration you can create additional named auth configurations to be used
|
||||
with features like `actions`. For example while your main GraphQL endpoint uses JWT for authentication you may want to use a header value to ensure your actions can only be called by clients having access to a shared secret
|
||||
or security header.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions is a very useful feature that is currently work in progress. For now the best use case for actions is to
|
||||
refresh database tables like materialized views or call a database procedure to refresh a cache table, etc. An action creates an http endpoint that anyone can call to have the SQL query executed. The below example will create an endpoint `/api/v1/actions/refresh_leaderboard_users` any request send to that endpoint will cause the sql query to be executed. the `auth_name` points to a named auth that should be used to secure this endpoint. In future we have big plans to allow your own custom code to run using actions.
|
||||
|
||||
```yaml
|
||||
actions:
|
||||
- name: refresh_leaderboard_users
|
||||
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||
auth_name: from_taskqueue
|
||||
```
|
||||
|
||||
#### Using CURL to test a query
|
||||
|
||||
```bash
|
||||
@ -1400,7 +1564,7 @@ roles:
|
||||
|
||||
This configuration is relatively simple to follow the `roles_query` parameter is the query that must be run to help figure out a users role. This query can be as complex as you like and include joins with other tables.
|
||||
|
||||
The individual roles are defined under the `roles` parameter and this includes each table the role has a custom setting for. The role is dynamically matched using the `match` parameter for example in the above case `users.id = 1` means that when the `roles_query` is executed a user with the id `1` willbe assigned the admin role and those that don't match get the `user` role if authenticated successfully or the `anon` role.
|
||||
The individual roles are defined under the `roles` parameter and this includes each table the role has a custom setting for. The role is dynamically matched using the `match` parameter for example in the above case `users.id = 1` means that when the `roles_query` is executed a user with the id `1` will be assigned the admin role and those that don't match get the `user` role if authenticated successfully or the `anon` role.
|
||||
|
||||
## Remote Joins
|
||||
|
||||
@ -1497,7 +1661,7 @@ tables:
|
||||
```
|
||||
|
||||
|
||||
## Configuration files
|
||||
## Configuration
|
||||
|
||||
Configuration files can either be in YAML or JSON their names are derived from the `GO_ENV` variable, for example `GO_ENV=prod` will cause the `prod.yaml` config file to be used. or `GO_ENV=dev` will use the `dev.yaml`. A path to look for the config files in can be specified using the `-path <folder>` command line argument.
|
||||
|
||||
@ -1593,13 +1757,29 @@ auth:
|
||||
# public_key_file: /secrets/public_key.pem
|
||||
# public_key_type: ecdsa #rsa
|
||||
|
||||
# header:
|
||||
# name: dnt
|
||||
# exists: true
|
||||
# value: localhost:8080
|
||||
|
||||
# You can add additional named auths to use with actions
|
||||
# In this example actions using this auth can only be
|
||||
# called from the Google Appengine Cron service that
|
||||
# sets a special header to all it's requests
|
||||
auths:
|
||||
- name: from_taskqueue
|
||||
type: header
|
||||
header:
|
||||
name: X-Appengine-Cron
|
||||
exists: true
|
||||
|
||||
database:
|
||||
type: postgres
|
||||
host: db
|
||||
port: 5432
|
||||
dbname: app_development
|
||||
user: postgres
|
||||
password: ''
|
||||
password: postgres
|
||||
|
||||
#schema: "public"
|
||||
#pool_size: 10
|
||||
@ -1623,6 +1803,17 @@ database:
|
||||
- encrypted
|
||||
- token
|
||||
|
||||
# Create custom actions with their own api endpoints
|
||||
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
|
||||
# A request to this url will execute the configured SQL query
|
||||
# which in this case refreshes a materialized view in the database.
|
||||
# The auth_name is from one of the configured auths
|
||||
actions:
|
||||
- name: refresh_leaderboard_users
|
||||
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||
auth_name: from_taskqueue
|
||||
|
||||
|
||||
tables:
|
||||
- name: customers
|
||||
remotes:
|
||||
@ -1723,9 +1914,74 @@ SG_AUTH_RAILS_REDIS_PASSWORD
|
||||
SG_AUTH_JWT_PUBLIC_KEY_FILE
|
||||
```
|
||||
|
||||
## YugabyteDB
|
||||
|
||||
Yugabyte is an open-source, geo-distrubuted cloud-native relational DB that scales horizontally. Super Graph works with Yugabyte right out of the box. If you think you're data needs will outgrow Postgres and you don't really want to deal with sharding then Yugabyte is the way to go. Just point Super Graph to your Yugabyte DB and everything will just work including running migrations, seeding, querying, mutations, etc.
|
||||
|
||||
To use Yugabyte in your local development flow just uncomment the following lines in the `docker-compose.yml` file that is part of your Super Graph app. Also remember to comment out the originl postgres `db` config.
|
||||
|
||||
```yaml
|
||||
# Postgres DB
|
||||
# db:
|
||||
# image: postgres:latest
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
|
||||
#Standard config to run a single node of Yugabyte
|
||||
yb-master:
|
||||
image: yugabytedb/yugabyte:latest
|
||||
container_name: yb-master-n1
|
||||
command: [ "/home/yugabyte/bin/yb-master",
|
||||
"--fs_data_dirs=/mnt/disk0,/mnt/disk1",
|
||||
"--master_addresses=yb-master-n1:7100",
|
||||
"--replication_factor=1",
|
||||
"--enable_ysql=true"]
|
||||
ports:
|
||||
- "7000:7000"
|
||||
environment:
|
||||
SERVICE_7000_NAME: yb-master
|
||||
|
||||
db:
|
||||
image: yugabytedb/yugabyte:latest
|
||||
container_name: yb-tserver-n1
|
||||
command: [ "/home/yugabyte/bin/yb-tserver",
|
||||
"--fs_data_dirs=/mnt/disk0,/mnt/disk1",
|
||||
"--start_pgsql_proxy",
|
||||
"--tserver_master_addrs=yb-master-n1:7100"]
|
||||
ports:
|
||||
- "9042:9042"
|
||||
- "6379:6379"
|
||||
- "5433:5433"
|
||||
- "9000:9000"
|
||||
environment:
|
||||
SERVICE_5433_NAME: ysql
|
||||
SERVICE_9042_NAME: ycql
|
||||
SERVICE_6379_NAME: yedis
|
||||
SERVICE_9000_NAME: yb-tserver
|
||||
depends_on:
|
||||
- yb-master
|
||||
|
||||
# Environment variables to point Super Graph to Yugabyte
|
||||
# This is required since it uses a different user and port number
|
||||
yourapp_api:
|
||||
image: dosco/super-graph:latest
|
||||
environment:
|
||||
GO_ENV: "development"
|
||||
Uncomment below for Yugabyte DB
|
||||
SG_DATABASE_PORT: 5433
|
||||
SG_DATABASE_USER: yugabyte
|
||||
SG_DATABASE_PASSWORD: yugabyte
|
||||
volumes:
|
||||
- ./config:/config
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- db
|
||||
```
|
||||
|
||||
## Developing Super Graph
|
||||
|
||||
If you want to build and run Super Graph from code then the below commands will build the web ui and launch Super Graph in developer mode with a watcher to rebuild on code changes. And the demo rails app is also launched to make it essier to test changes.
|
||||
If you want to build and run Super Graph from code then the below commands will build the web ui and launch Super Graph in developer mode with a watcher to rebuild on code changes. And the demo rails app is also launched to make it easier to test changes.
|
||||
|
||||
```bash
|
||||
|
||||
|
@ -13,7 +13,7 @@ Super Graph code is made up of a number of packages. We have done our best to ke
|
||||
|
||||
## QCODE
|
||||
|
||||
This package contains the core of the GraphQL conpiler it handling the lexing and parsing of the GraphQL query transforming it into an internal representation called
|
||||
This package contains the core of the GraphQL compiler it handling the lexing and parsing of the GraphQL query transforming it into an internal representation called
|
||||
`QCode`.
|
||||
|
||||
This is the first step of the compiling process the `func NewCompiler(c Config)` function creates a new instance of this compiler which has it's own config.
|
||||
@ -71,7 +71,7 @@ item{itemObjOpen, 16, 20} // {
|
||||
...
|
||||
```
|
||||
|
||||
These tokens are then fed into the parser `parse.go` the parser does the work of generating an abstract syntax tree (AST) from the tokens. This AST is an internal representation (data structure) and is not exposed outside the package. Sinc the AST is a tree a stack `stack.go` is used to walk the tree and generate the QCode AST. The QCode data structure is also a tree (represented as an array). This is then returned to the caller of the compile function.
|
||||
These tokens are then fed into the parser `parse.go` the parser does the work of generating an abstract syntax tree (AST) from the tokens. This AST is an internal representation (data structure) and is not exposed outside the package. Since the AST is a tree a stack `stack.go` is used to walk the tree and generate the QCode AST. The QCode data structure is also a tree (represented as an array). This is then returned to the caller of the compile function.
|
||||
|
||||
```go
|
||||
type Operation struct {
|
||||
|
@ -19,7 +19,7 @@ default: &default
|
||||
encoding: unicode
|
||||
host: db
|
||||
username: postgres
|
||||
password:
|
||||
password: postgres
|
||||
pool: 5
|
||||
|
||||
development:
|
||||
|
2
go.mod
2
go.mod
@ -2,7 +2,6 @@ module github.com/dosco/super-graph
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.rice v1.0.0
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||
@ -12,7 +11,6 @@ require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/dlclark/regexp2 v1.2.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/garyburd/redigo v1.6.0
|
||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
|
||||
|
4
go.sum
4
go.sum
@ -5,8 +5,6 @@ github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@ -54,8 +52,6 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw=
|
||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c h1:/bXaeEuNG6V0HeyEGw11DYLW5BGsOPlcVRIXbHNUWSo=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||
|
@ -27,9 +27,14 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
||||
|
||||
var k []byte
|
||||
state := expectKey
|
||||
instr := false
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
if state == expectObjClose || state == expectListClose {
|
||||
if b[i-1] != '\\' && b[i] == '"' {
|
||||
instr = !instr
|
||||
}
|
||||
if !instr {
|
||||
switch b[i] {
|
||||
case '{', '[':
|
||||
d++
|
||||
@ -37,6 +42,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
||||
d--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state == expectKey {
|
||||
switch b[i] {
|
||||
@ -109,7 +115,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
||||
case state == expectValue && b[i] == 'n':
|
||||
state = expectNull
|
||||
|
||||
case state == expectNull && b[i] == 'l':
|
||||
case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
|
||||
e = i
|
||||
}
|
||||
|
||||
|
10
jsn/get.go
10
jsn/get.go
@ -51,8 +51,14 @@ func Get(b []byte, keys [][]byte) []Field {
|
||||
state := expectKey
|
||||
|
||||
n := 0
|
||||
instr := false
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
if state == expectObjClose || state == expectListClose {
|
||||
if b[i-1] != '\\' && b[i] == '"' {
|
||||
instr = !instr
|
||||
}
|
||||
if !instr {
|
||||
switch b[i] {
|
||||
case '{', '[':
|
||||
d++
|
||||
@ -60,6 +66,7 @@ func Get(b []byte, keys [][]byte) []Field {
|
||||
d--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case state == expectKey && b[i] == '"':
|
||||
@ -117,8 +124,9 @@ func Get(b []byte, keys [][]byte) []Field {
|
||||
|
||||
case state == expectValue && b[i] == 'n':
|
||||
state = expectNull
|
||||
s = i
|
||||
|
||||
case state == expectNull && b[i] == 'l':
|
||||
case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
|
||||
e = i
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package jsn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -158,6 +159,11 @@ var (
|
||||
|
||||
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",}`
|
||||
|
||||
input6 = `
|
||||
{"users" : [{"id" : 1, "email" : "vicram@gmail.com", "slug" : "vikram-rangnekar", "threads" : [], "threads_cursor" : null}, {"id" : 3, "email" : "marareilly@lang.name", "slug" : "raymundo-corwin", "threads" : [{"id" : 9, "title" : "Et alias et aut porro praesentium nam in voluptatem reiciendis quisquam perspiciatis inventore eos quia et et enim qui amet."}, {"id" : 25, "title" : "Ipsam quam nemo culpa tempore amet optio sit sed eligendi autem consequatur quaerat rem velit quibusdam quibusdam optio a voluptatem."}], "threads_cursor" : 25}], "users_cursor" : 3}`
|
||||
|
||||
input7, _ = ioutil.ReadFile("test.json")
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
@ -227,6 +233,41 @@ func TestGet1(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet2(t *testing.T) {
|
||||
values := Get([]byte(input6), [][]byte{
|
||||
[]byte("users_cursor"), []byte("threads_cursor"),
|
||||
})
|
||||
|
||||
expected := []Field{
|
||||
{[]byte("threads_cursor"), []byte(`null`)},
|
||||
{[]byte("threads_cursor"), []byte(`25`)},
|
||||
{[]byte("users_cursor"), []byte(`3`)},
|
||||
}
|
||||
|
||||
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 TestGet3(t *testing.T) {
|
||||
values := Get(input7, [][]byte{[]byte("data")})
|
||||
v := values[0].Value
|
||||
|
||||
if !bytes.Equal(v[len(v)-11:], []byte(`Rangnekar"}`)) {
|
||||
t.Fatal("corrupt ending")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValue(t *testing.T) {
|
||||
v1 := []byte("12345")
|
||||
if !bytes.Equal(Value(v1), v1) {
|
||||
|
10
jsn/keys.go
10
jsn/keys.go
@ -10,10 +10,14 @@ func Keys(b []byte) [][]byte {
|
||||
|
||||
st := NewStack()
|
||||
ae := 0
|
||||
instr := false
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
|
||||
if state == expectObjClose || state == expectListClose {
|
||||
if b[i-1] != '\\' && b[i] == '"' {
|
||||
instr = !instr
|
||||
}
|
||||
if !instr {
|
||||
switch b[i] {
|
||||
case '{', '[':
|
||||
d++
|
||||
@ -21,6 +25,7 @@ func Keys(b []byte) [][]byte {
|
||||
d--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
si := st.Peek()
|
||||
|
||||
@ -101,8 +106,9 @@ func Keys(b []byte) [][]byte {
|
||||
|
||||
case state == expectValue && b[i] == 'n':
|
||||
state = expectNull
|
||||
s = i
|
||||
|
||||
case state == expectNull && b[i] == 'l':
|
||||
case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
|
||||
e = i
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,8 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
||||
state := expectKey
|
||||
ws, we := -1, len(b)
|
||||
|
||||
instr := false
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
// skip any left padding whitespace
|
||||
if ws == -1 && (b[i] == '{' || b[i] == '[') {
|
||||
@ -39,6 +41,10 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
||||
}
|
||||
|
||||
if state == expectObjClose || state == expectListClose {
|
||||
if b[i-1] != '\\' && b[i] == '"' {
|
||||
instr = !instr
|
||||
}
|
||||
if !instr {
|
||||
switch b[i] {
|
||||
case '{', '[':
|
||||
d++
|
||||
@ -46,6 +52,7 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
||||
d--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case state == expectKey && b[i] == '"':
|
||||
@ -104,8 +111,9 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
||||
|
||||
case state == expectValue && b[i] == 'n':
|
||||
state = expectNull
|
||||
s = i
|
||||
|
||||
case state == expectNull && b[i] == 'l':
|
||||
case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
|
||||
e = i
|
||||
}
|
||||
|
||||
|
@ -11,9 +11,14 @@ func Strip(b []byte, path [][]byte) []byte {
|
||||
pi := 0
|
||||
pm := false
|
||||
state := expectKey
|
||||
instr := false
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
if state == expectObjClose || state == expectListClose {
|
||||
if b[i-1] != '\\' && b[i] == '"' {
|
||||
instr = !instr
|
||||
}
|
||||
if !instr {
|
||||
switch b[i] {
|
||||
case '{', '[':
|
||||
d++
|
||||
@ -21,6 +26,7 @@ func Strip(b []byte, path [][]byte) []byte {
|
||||
d--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case state == expectKey && b[i] == '"':
|
||||
@ -82,8 +88,9 @@ func Strip(b []byte, path [][]byte) []byte {
|
||||
|
||||
case state == expectValue && b[i] == 'n':
|
||||
state = expectNull
|
||||
s = i
|
||||
|
||||
case state == expectNull && b[i] == 'l':
|
||||
case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'):
|
||||
e = i
|
||||
}
|
||||
|
||||
|
1
jsn/test.json
Normal file
1
jsn/test.json
Normal file
File diff suppressed because one or more lines are too long
@ -257,7 +257,7 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
|
||||
ctx := context.Background()
|
||||
// Lock to ensure multiple migrations cannot occur simultaneously
|
||||
lockNum := int64(9628173550095224) // arbitrary random number
|
||||
if _, lockErr := m.conn.Exec(ctx, "select pg_advisory_lock($1)", lockNum); lockErr != nil {
|
||||
if _, lockErr := m.conn.Exec(ctx, "select pg_try_advisory_lock($1)", lockNum); lockErr != nil {
|
||||
return lockErr
|
||||
}
|
||||
defer func() {
|
||||
@ -331,9 +331,9 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
|
||||
}
|
||||
|
||||
// Reset all database connection settings. Important to do before updating version as search_path may have been changed.
|
||||
if _, err := tx.Exec(ctx, "reset all"); err != nil {
|
||||
return err
|
||||
}
|
||||
// if _, err := tx.Exec(ctx, "reset all"); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// Add one to the version
|
||||
_, err = tx.Exec(ctx, "update "+m.versionTable+" set version=$1", sequence)
|
||||
|
198
psql/columns.go
Normal file
198
psql/columns.go
Normal file
@ -0,0 +1,198 @@
|
||||
//nolint:errcheck
|
||||
package psql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
)
|
||||
|
||||
func (c *compilerContext) renderBaseColumns(
|
||||
sel *qcode.Select,
|
||||
ti *DBTableInfo,
|
||||
childCols []*qcode.Column,
|
||||
skipped uint32) ([]int, bool, error) {
|
||||
|
||||
var realColsRendered []int
|
||||
|
||||
colcount := (len(sel.Cols) + len(sel.OrderBy) + 1)
|
||||
colmap := make(map[string]struct{}, colcount)
|
||||
|
||||
isSearch := sel.Args["search"] != nil
|
||||
isCursorPaged := sel.Paging.Type != qcode.PtOffset
|
||||
isAgg := false
|
||||
|
||||
i := 0
|
||||
for n, col := range sel.Cols {
|
||||
cn := col.Name
|
||||
colmap[cn] = struct{}{}
|
||||
|
||||
_, isRealCol := ti.ColMap[cn]
|
||||
|
||||
if isRealCol {
|
||||
c.renderComma(i)
|
||||
realColsRendered = append(realColsRendered, n)
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if isSearch && !isRealCol {
|
||||
switch {
|
||||
case cn == "search_rank":
|
||||
if err := c.renderColumnSearchRank(sel, ti, col, i); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
i++
|
||||
|
||||
case strings.HasPrefix(cn, "search_headline_"):
|
||||
if err := c.renderColumnSearchHeadline(sel, ti, col, i); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
i++
|
||||
|
||||
}
|
||||
} else {
|
||||
if err := c.renderColumnFunction(sel, ti, col, i); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
isAgg = true
|
||||
i++
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if isCursorPaged {
|
||||
if _, ok := colmap[ti.PrimaryCol.Key]; !ok {
|
||||
colmap[ti.PrimaryCol.Key] = struct{}{}
|
||||
c.renderComma(i)
|
||||
colWithTable(c.w, ti.Name, ti.PrimaryCol.Name)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
for _, ob := range sel.OrderBy {
|
||||
if _, ok := colmap[ob.Col]; ok {
|
||||
continue
|
||||
}
|
||||
colmap[ob.Col] = struct{}{}
|
||||
c.renderComma(i)
|
||||
colWithTable(c.w, ti.Name, ob.Col)
|
||||
i++
|
||||
}
|
||||
|
||||
for _, col := range childCols {
|
||||
if _, ok := colmap[col.Name]; ok {
|
||||
continue
|
||||
}
|
||||
c.renderComma(i)
|
||||
colWithTable(c.w, col.Table, col.Name)
|
||||
i++
|
||||
}
|
||||
|
||||
return realColsRendered, isAgg, nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderColumnSearchRank(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
|
||||
if isColumnBlocked(sel, col.Name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ti.TSVCol == nil {
|
||||
return errors.New("no ts_vector column found")
|
||||
}
|
||||
cn := ti.TSVCol.Name
|
||||
arg := sel.Args["search"]
|
||||
|
||||
c.renderComma(columnsRendered)
|
||||
//fmt.Fprintf(w, `ts_rank("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
|
||||
//c.sel.Name, cn, arg.Val, col.Name)
|
||||
io.WriteString(c.w, `ts_rank(`)
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
if c.schema.ver >= 110000 {
|
||||
io.WriteString(c.w, `, websearch_to_tsquery('{{`)
|
||||
} else {
|
||||
io.WriteString(c.w, `, to_tsquery('{{`)
|
||||
}
|
||||
io.WriteString(c.w, arg.Val)
|
||||
io.WriteString(c.w, `}}'))`)
|
||||
alias(c.w, col.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderColumnSearchHeadline(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
|
||||
cn := col.Name[16:]
|
||||
|
||||
if isColumnBlocked(sel, cn) {
|
||||
return nil
|
||||
}
|
||||
arg := sel.Args["search"]
|
||||
|
||||
c.renderComma(columnsRendered)
|
||||
//fmt.Fprintf(w, `ts_headline("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
|
||||
//c.sel.Name, cn, arg.Val, col.Name)
|
||||
io.WriteString(c.w, `ts_headline(`)
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
if c.schema.ver >= 110000 {
|
||||
io.WriteString(c.w, `, websearch_to_tsquery('{{`)
|
||||
} else {
|
||||
io.WriteString(c.w, `, to_tsquery('{{`)
|
||||
}
|
||||
io.WriteString(c.w, arg.Val)
|
||||
io.WriteString(c.w, `}}'))`)
|
||||
alias(c.w, col.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderColumnFunction(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
|
||||
pl := funcPrefixLen(col.Name)
|
||||
// if pl == 0 {
|
||||
// //fmt.Fprintf(w, `'%s not defined' AS %s`, cn, col.Name)
|
||||
// io.WriteString(c.w, `'`)
|
||||
// io.WriteString(c.w, col.Name)
|
||||
// io.WriteString(c.w, ` not defined'`)
|
||||
// alias(c.w, col.Name)
|
||||
// }
|
||||
|
||||
if pl == 0 || !sel.Functions {
|
||||
return nil
|
||||
}
|
||||
|
||||
cn := col.Name[pl:]
|
||||
|
||||
if isColumnBlocked(sel, cn) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fn := cn[0 : pl-1]
|
||||
|
||||
c.renderComma(columnsRendered)
|
||||
|
||||
//fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Name, cn, col.Name)
|
||||
io.WriteString(c.w, fn)
|
||||
io.WriteString(c.w, `(`)
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
io.WriteString(c.w, `)`)
|
||||
alias(c.w, col.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderComma(columnsRendered int) {
|
||||
if columnsRendered != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
}
|
||||
|
||||
func isColumnBlocked(sel *qcode.Select, name string) bool {
|
||||
if len(sel.Allowed) != 0 {
|
||||
if _, ok := sel.Allowed[name]; !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -12,68 +12,41 @@ func simpleInsert(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
vars := map[string]json.RawMessage{
|
||||
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "user")
|
||||
}
|
||||
|
||||
func singleInsert(t *testing.T) {
|
||||
gql := `mutation {
|
||||
product(id: 15, insert: $insert) {
|
||||
product(id: $id, insert: $insert) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT "t"."name", "t"."description", "t"."price", "t"."user_id" FROM "_sg_input" i, 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") 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{
|
||||
"insert": json.RawMessage(` { "name": "my_name", "price": 6.95, "description": "my_desc", "user_id": 5 }`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "anon")
|
||||
}
|
||||
|
||||
func bulkInsert(t *testing.T) {
|
||||
gql := `mutation {
|
||||
product(name: "test", id: 15, insert: $insert) {
|
||||
product(name: "test", id: $id, insert: $insert) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(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") 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{
|
||||
"insert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "anon")
|
||||
}
|
||||
|
||||
func simpleInsertWithPresets(t *testing.T) {
|
||||
@ -83,20 +56,11 @@ func simpleInsertWithPresets(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM "_sg_input" i, 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") 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": "Tomato", "price": 5.76}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "user")
|
||||
}
|
||||
|
||||
func nestedInsertManyToMany(t *testing.T) {
|
||||
@ -118,10 +82,6 @@ func nestedInsertManyToMany(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "product_id", "customer_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "products"."id", "customers"."id" FROM "_sg_input" i, "products", "customers", json_populate_record(NULL::purchases, i.j) t RETURNING *) 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), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products", json_populate_record(NULL::purchases, i.j) t RETURNING *) 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(` {
|
||||
"sale_type": "bought",
|
||||
@ -139,16 +99,7 @@ func nestedInsertManyToMany(t *testing.T) {
|
||||
`),
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql1 && string(resSQL) != sql2 {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedInsertOneToMany(t *testing.T) {
|
||||
@ -165,8 +116,6 @@ func nestedInsertOneToMany(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 (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->'product') t RETURNING *) 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(`{
|
||||
"email": "thedude@rug.com",
|
||||
@ -182,14 +131,7 @@ func nestedInsertOneToMany(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedInsertOneToOne(t *testing.T) {
|
||||
@ -205,8 +147,6 @@ func nestedInsertOneToOne(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->'user') t RETURNING *), "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",
|
||||
@ -225,14 +165,7 @@ func nestedInsertOneToOne(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedInsertOneToManyWithConnect(t *testing.T) {
|
||||
@ -249,8 +182,6 @@ 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"= ((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(`{
|
||||
"email": "thedude@rug.com",
|
||||
@ -263,14 +194,7 @@ func nestedInsertOneToManyWithConnect(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedInsertOneToOneWithConnect(t *testing.T) {
|
||||
@ -290,8 +214,6 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user", "tags_2_join"."json_2" AS "tags") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_2"), '[]') AS "json_2" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "tags_2"."id" AS "id", "tags_2"."name" AS "name") AS "json_row_2")) AS "json_2" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2" LIMIT ('20') :: integer) AS "json_agg_2") AS "tags_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
vars := map[string]json.RawMessage{
|
||||
"data": json.RawMessage(`{
|
||||
"name": "Apple",
|
||||
@ -304,14 +226,7 @@ func nestedInsertOneToOneWithConnect(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedInsertOneToOneWithConnectArray(t *testing.T) {
|
||||
@ -327,8 +242,6 @@ func nestedInsertOneToOneWithConnectArray(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id" = ANY((select a::bigint AS list from json_array_elements_text((i.j->'user'->'connect'->>'id')::json) AS a)) LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
vars := map[string]json.RawMessage{
|
||||
"data": json.RawMessage(`{
|
||||
"name": "Apple",
|
||||
@ -341,14 +254,7 @@ func nestedInsertOneToOneWithConnectArray(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func TestCompileInsert(t *testing.T) {
|
||||
@ -362,5 +268,4 @@ func TestCompileInsert(t *testing.T) {
|
||||
t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect)
|
||||
t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect)
|
||||
t.Run("nestedInsertOneToOneWithConnectArray", nestedInsertOneToOneWithConnectArray)
|
||||
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables
|
||||
root.Where = nil
|
||||
root.Args = nil
|
||||
|
||||
return c.compileQuery(qc, w)
|
||||
return c.compileQuery(qc, w, vars)
|
||||
}
|
||||
|
||||
type kvitem struct {
|
||||
@ -682,12 +682,6 @@ func renderCteNameWithSuffix(w io.Writer, item kvitem, suffix string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func quoted(w io.Writer, identifier string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, identifier)
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func joinPath(w io.Writer, path []string) {
|
||||
for i := range path {
|
||||
if i != 0 {
|
||||
|
@ -13,20 +13,11 @@ func singleUpsert(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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") 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{
|
||||
"upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "user")
|
||||
}
|
||||
|
||||
func singleUpsertWhere(t *testing.T) {
|
||||
@ -37,20 +28,11 @@ func singleUpsertWhere(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) WHERE (("products"."price") > 3) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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") 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{
|
||||
"upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "user")
|
||||
}
|
||||
|
||||
func bulkUpsert(t *testing.T) {
|
||||
@ -61,20 +43,11 @@ func bulkUpsert(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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") 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{
|
||||
"upsert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "user")
|
||||
}
|
||||
|
||||
func delete(t *testing.T) {
|
||||
@ -85,20 +58,11 @@ func delete(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") IS NOT DISTINCT FROM 1)) 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" }`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "user")
|
||||
}
|
||||
|
||||
// func blockedInsert(t *testing.T) {
|
||||
|
@ -1,8 +1,11 @@
|
||||
package psql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
@ -10,11 +13,14 @@ import (
|
||||
|
||||
const (
|
||||
errNotExpected = "Generated SQL did not match what was expected"
|
||||
headerMarker = "=== RUN"
|
||||
commentMarker = "---"
|
||||
)
|
||||
|
||||
var (
|
||||
qcompile *qcode.Compiler
|
||||
pcompile *Compiler
|
||||
expected map[string][]string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@ -138,21 +144,94 @@ func TestMain(m *testing.M) {
|
||||
Vars: vars,
|
||||
})
|
||||
|
||||
expected = make(map[string][]string)
|
||||
|
||||
b, err := ioutil.ReadFile("tests.sql")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
text := string(b)
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
var h string
|
||||
|
||||
for _, v := range lines {
|
||||
switch {
|
||||
case strings.HasPrefix(v, headerMarker):
|
||||
h = strings.TrimSpace(v[len(headerMarker):])
|
||||
|
||||
case strings.HasPrefix(v, commentMarker):
|
||||
break
|
||||
|
||||
default:
|
||||
v := strings.TrimSpace(v)
|
||||
if len(v) != 0 {
|
||||
expected[h] = append(expected[h], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func compileGQLToPSQL(gql string, vars Variables, role string) ([]byte, error) {
|
||||
func compileGQLToPSQL(t *testing.T, gql string, vars Variables, role string) {
|
||||
generateTestFile := false
|
||||
|
||||
if generateTestFile {
|
||||
var sqlStmts []string
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
qc, err := qcompile.Compile([]byte(gql), role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, sqlB, err := pcompile.CompileEx(qc, vars)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sql := string(sqlB)
|
||||
|
||||
match := false
|
||||
for _, s := range sqlStmts {
|
||||
if sql == s {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
s := string(sql)
|
||||
sqlStmts = append(sqlStmts, s)
|
||||
fmt.Println(s)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < 200; i++ {
|
||||
qc, err := qcompile.Compile([]byte(gql), role)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, sqlStmt, err := pcompile.CompileEx(qc, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//fmt.Println(string(sqlStmt))
|
||||
failed := true
|
||||
|
||||
return sqlStmt, nil
|
||||
for _, sql := range expected[t.Name()] {
|
||||
if string(sqlStmt) == sql {
|
||||
failed = false
|
||||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
fmt.Println(string(sqlStmt))
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
744
psql/query.go
744
psql/query.go
@ -30,7 +30,10 @@ type Compiler struct {
|
||||
}
|
||||
|
||||
func NewCompiler(conf Config) *Compiler {
|
||||
return &Compiler{conf.Schema, conf.Vars}
|
||||
return &Compiler{
|
||||
schema: conf.Schema,
|
||||
vars: conf.Vars,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error {
|
||||
@ -65,7 +68,7 @@ func (co *Compiler) CompileEx(qc *qcode.QCode, vars Variables) (uint32, []byte,
|
||||
func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
|
||||
switch qc.Type {
|
||||
case qcode.QTQuery:
|
||||
return co.compileQuery(qc, w)
|
||||
return co.compileQuery(qc, w, vars)
|
||||
case qcode.QTInsert, qcode.QTUpdate, qcode.QTDelete, qcode.QTUpsert:
|
||||
return co.compileMutation(qc, w, vars)
|
||||
}
|
||||
@ -73,22 +76,19 @@ func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint3
|
||||
return 0, fmt.Errorf("Unknown operation type %d", qc.Type)
|
||||
}
|
||||
|
||||
func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
|
||||
if len(qc.Selects) == 0 {
|
||||
return 0, errors.New("empty query")
|
||||
}
|
||||
|
||||
c := &compilerContext{w, qc.Selects, co}
|
||||
multiRoot := (len(qc.Roots) > 1)
|
||||
|
||||
st := NewIntStack()
|
||||
si := 0
|
||||
|
||||
if multiRoot {
|
||||
io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `)
|
||||
i := 0
|
||||
|
||||
io.WriteString(c.w, `SELECT json_build_object(`)
|
||||
for _, id := range qc.Roots {
|
||||
root := qc.Selects[id]
|
||||
root := &qc.Selects[id]
|
||||
if root.SkipRender {
|
||||
continue
|
||||
}
|
||||
@ -96,44 +96,17 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
st.Push(root.ID + closeBlock)
|
||||
st.Push(root.ID)
|
||||
|
||||
if si != 0 {
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `"sel_`)
|
||||
int2string(c.w, root.ID)
|
||||
io.WriteString(c.w, `"."json_`)
|
||||
int2string(c.w, root.ID)
|
||||
io.WriteString(c.w, `"`)
|
||||
|
||||
alias(c.w, root.FieldName)
|
||||
si++
|
||||
c.renderRootSelect(root)
|
||||
i++
|
||||
}
|
||||
|
||||
if si != 0 {
|
||||
io.WriteString(c.w, ` FROM `)
|
||||
io.WriteString(c.w, `) as "__root" 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)
|
||||
|
||||
st.Push(root.ID + closeBlock)
|
||||
st.Push(root.ID)
|
||||
|
||||
io.WriteString(c.w, `) FROM `)
|
||||
si++
|
||||
}
|
||||
}
|
||||
|
||||
if si == 0 {
|
||||
if i == 0 {
|
||||
return 0, errors.New("all tables skipped. cannot render query")
|
||||
}
|
||||
|
||||
@ -149,22 +122,22 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
if id < closeBlock {
|
||||
sel := &c.s[id]
|
||||
|
||||
if sel.ParentID == -1 {
|
||||
io.WriteString(c.w, `(`)
|
||||
}
|
||||
|
||||
ti, err := c.schema.GetTable(sel.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if sel.ParentID != -1 {
|
||||
if err = c.renderLateralJoin(sel); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if sel.ParentID == -1 {
|
||||
io.WriteString(c.w, `(`)
|
||||
} else {
|
||||
c.renderLateralJoin(sel)
|
||||
}
|
||||
|
||||
skipped, err := c.renderSelect(sel, ti)
|
||||
if !ti.Singular {
|
||||
c.renderPluralSelect(sel, ti)
|
||||
}
|
||||
|
||||
skipped, err := c.renderSelect(sel, ti, vars)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -191,40 +164,94 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
err = c.renderSelectClose(sel, ti)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
if !ti.Singular {
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "__sel", sel.ID)
|
||||
}
|
||||
|
||||
if sel.ParentID != -1 {
|
||||
if err = c.renderLateralJoinClose(sel); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
if sel.ParentID == -1 {
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, `sel`, sel.ID)
|
||||
aliasWithID(c.w, "__sel", sel.ID)
|
||||
|
||||
if st.Len() != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
} else {
|
||||
c.renderLateralJoinClose(sel)
|
||||
}
|
||||
|
||||
if len(sel.Args) != 0 {
|
||||
i := 0
|
||||
for _, v := range sel.Args {
|
||||
qcode.FreeNode(v)
|
||||
qcode.FreeNode(v, 500)
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if multiRoot {
|
||||
io.WriteString(c.w, `) AS "json_root"`)
|
||||
}
|
||||
|
||||
return ignored, nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (uint32, []*qcode.Column, error) {
|
||||
func (c *compilerContext) renderPluralSelect(sel *qcode.Select, ti *DBTableInfo) error {
|
||||
io.WriteString(c.w, `SELECT coalesce(json_agg("__sel_`)
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `"."json"), '[]') as "json"`)
|
||||
|
||||
if sel.Paging.Type != qcode.PtOffset {
|
||||
n := 0
|
||||
|
||||
// check if primary key already included in order by
|
||||
// query argument
|
||||
for _, ob := range sel.OrderBy {
|
||||
if ob.Col == ti.PrimaryCol.Key {
|
||||
n = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
n = len(sel.OrderBy)
|
||||
} else {
|
||||
n = len(sel.OrderBy) + 1
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `, CONCAT_WS(','`)
|
||||
for i := 0; i < n; i++ {
|
||||
io.WriteString(c.w, `, max("__cur_`)
|
||||
int2string(c.w, int32(i))
|
||||
io.WriteString(c.w, `")`)
|
||||
}
|
||||
io.WriteString(c.w, `) as "cursor"`)
|
||||
}
|
||||
|
||||
io.WriteString(c.w, ` FROM (`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderRootSelect(sel *qcode.Select) error {
|
||||
io.WriteString(c.w, `'`)
|
||||
io.WriteString(c.w, sel.FieldName)
|
||||
io.WriteString(c.w, `', `)
|
||||
|
||||
io.WriteString(c.w, `"__sel_`)
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `"."json"`)
|
||||
|
||||
if sel.Paging.Type != qcode.PtOffset {
|
||||
io.WriteString(c.w, `, '`)
|
||||
io.WriteString(c.w, sel.FieldName)
|
||||
io.WriteString(c.w, `_cursor', `)
|
||||
|
||||
io.WriteString(c.w, `"__sel_`)
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `"."cursor"`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) initSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) (uint32, []*qcode.Column, error) {
|
||||
var skipped uint32
|
||||
|
||||
cols := make([]*qcode.Column, 0, len(sel.Cols))
|
||||
@ -238,6 +265,31 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
|
||||
colmap[sel.OrderBy[i].Col] = struct{}{}
|
||||
}
|
||||
|
||||
if sel.Paging.Type != qcode.PtOffset {
|
||||
colmap[ti.PrimaryCol.Key] = struct{}{}
|
||||
addPrimaryKey := true
|
||||
|
||||
for _, ob := range sel.OrderBy {
|
||||
if ob.Col == ti.PrimaryCol.Key {
|
||||
addPrimaryKey = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if addPrimaryKey {
|
||||
ob := &qcode.OrderBy{Col: ti.PrimaryCol.Name, Order: qcode.OrderAsc}
|
||||
|
||||
if sel.Paging.Type == qcode.PtBackward {
|
||||
ob.Order = qcode.OrderDesc
|
||||
}
|
||||
sel.OrderBy = append(sel.OrderBy, ob)
|
||||
}
|
||||
}
|
||||
|
||||
if sel.Paging.Cursor {
|
||||
c.addSeekPredicate(sel)
|
||||
}
|
||||
|
||||
for _, id := range sel.Children {
|
||||
child := &c.s[id]
|
||||
|
||||
@ -283,7 +335,73 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
|
||||
return skipped, cols, nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint32, error) {
|
||||
// This
|
||||
// (A, B, C) >= (X, Y, Z)
|
||||
//
|
||||
// Becomes
|
||||
// (A > X)
|
||||
// OR ((A = X) AND (B > Y))
|
||||
// OR ((A = X) AND (B = Y) AND (C > Z))
|
||||
// OR ((A = X) AND (B = Y) AND (C = Z))
|
||||
|
||||
func (c *compilerContext) addSeekPredicate(sel *qcode.Select) error {
|
||||
var or, and *qcode.Exp
|
||||
|
||||
obLen := len(sel.OrderBy)
|
||||
|
||||
if obLen > 1 {
|
||||
or = qcode.NewFilter()
|
||||
or.Op = qcode.OpOr
|
||||
}
|
||||
|
||||
for i := 0; i < obLen; i++ {
|
||||
if i > 0 {
|
||||
and = qcode.NewFilter()
|
||||
and.Op = qcode.OpAnd
|
||||
}
|
||||
|
||||
for n, ob := range sel.OrderBy {
|
||||
f := qcode.NewFilter()
|
||||
f.Col = ob.Col
|
||||
f.Type = qcode.ValRef
|
||||
f.Table = "__cur"
|
||||
f.Val = ob.Col
|
||||
|
||||
if obLen == 1 {
|
||||
qcode.AddFilter(sel, f)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case i > 0 && n != i:
|
||||
f.Op = qcode.OpEquals
|
||||
case ob.Order == qcode.OrderDesc:
|
||||
f.Op = qcode.OpLesserThan
|
||||
default:
|
||||
f.Op = qcode.OpGreaterThan
|
||||
}
|
||||
|
||||
if and != nil {
|
||||
and.Children = append(and.Children, f)
|
||||
} else {
|
||||
or.Children = append(or.Children, f)
|
||||
}
|
||||
|
||||
if n == i {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if and != nil {
|
||||
or.Children = append(or.Children, and)
|
||||
}
|
||||
}
|
||||
|
||||
qcode.AddFilter(sel, or)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) (uint32, error) {
|
||||
var rel *DBRel
|
||||
var err error
|
||||
|
||||
@ -296,131 +414,53 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
||||
}
|
||||
}
|
||||
|
||||
skipped, childCols, err := c.processChildren(sel, ti)
|
||||
skipped, childCols, err := c.initSelect(sel, ti, vars)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
hasOrder := len(sel.OrderBy) != 0
|
||||
|
||||
// SELECT
|
||||
if !ti.Singular {
|
||||
//fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Name)
|
||||
io.WriteString(c.w, `SELECT coalesce(json_agg("`)
|
||||
io.WriteString(c.w, "json_")
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `"`)
|
||||
|
||||
if hasOrder {
|
||||
if err := c.renderOrderBy(sel, ti); err != nil {
|
||||
io.WriteString(c.w, `SELECT json_build_object(`)
|
||||
if err := c.renderColumns(sel, ti, skipped); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
io.WriteString(c.w, `) AS "json"`)
|
||||
|
||||
if sel.Paging.Type != qcode.PtOffset {
|
||||
for i, ob := range sel.OrderBy {
|
||||
io.WriteString(c.w, `, LAST_VALUE(`)
|
||||
colWithTableID(c.w, ti.Name, sel.ID, ob.Col)
|
||||
io.WriteString(c.w, `) OVER() AS "__cur_`)
|
||||
int2string(c.w, int32(i))
|
||||
io.WriteString(c.w, `"`)
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Name)
|
||||
io.WriteString(c.w, `), '[]')`)
|
||||
aliasWithID(c.w, "json", sel.ID)
|
||||
io.WriteString(c.w, ` FROM (`)
|
||||
}
|
||||
|
||||
// ROW-TO-JSON
|
||||
io.WriteString(c.w, `SELECT `)
|
||||
|
||||
if len(sel.DistinctOn) != 0 {
|
||||
c.renderDistinctOn(sel, ti)
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `row_to_json((`)
|
||||
|
||||
//fmt.Fprintf(w, `SELECT "%d" FROM (SELECT `, c.sel.ID)
|
||||
io.WriteString(c.w, `SELECT "json_row_`)
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `" FROM (SELECT `)
|
||||
|
||||
// Combined column names
|
||||
c.renderColumns(sel, ti)
|
||||
|
||||
c.renderRemoteRelColumns(sel, ti)
|
||||
|
||||
if err = c.renderJoinedColumns(sel, ti, skipped); err != nil {
|
||||
return skipped, err
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, `) AS "%d"`, c.sel.ID)
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "json_row", sel.ID)
|
||||
|
||||
//fmt.Fprintf(w, `)) AS "%s"`, c.sel.Name)
|
||||
io.WriteString(c.w, `))`)
|
||||
aliasWithID(c.w, "json", sel.ID)
|
||||
// END-ROW-TO-JSON
|
||||
|
||||
if hasOrder {
|
||||
c.renderOrderByColumns(sel, ti)
|
||||
}
|
||||
// END-SELECT
|
||||
|
||||
// FROM (SELECT .... )
|
||||
err = c.renderBaseSelect(sel, ti, rel, childCols, skipped)
|
||||
if err != nil {
|
||||
return skipped, err
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID)
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, ti.Name, sel.ID)
|
||||
|
||||
// END-FROM
|
||||
|
||||
return skipped, nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo) error {
|
||||
hasOrder := len(sel.OrderBy) != 0
|
||||
|
||||
if hasOrder {
|
||||
err := c.renderOrderBy(sel, ti)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case ti.Singular:
|
||||
io.WriteString(c.w, ` LIMIT ('1') :: integer`)
|
||||
|
||||
case len(sel.Paging.Limit) != 0:
|
||||
//fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit)
|
||||
io.WriteString(c.w, ` LIMIT ('`)
|
||||
io.WriteString(c.w, sel.Paging.Limit)
|
||||
io.WriteString(c.w, `') :: integer`)
|
||||
|
||||
case sel.Paging.NoLimit:
|
||||
break
|
||||
|
||||
default:
|
||||
io.WriteString(c.w, ` LIMIT ('20') :: integer`)
|
||||
}
|
||||
|
||||
if len(sel.Paging.Offset) != 0 {
|
||||
//fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, c.sel.Paging.Offset)
|
||||
io.WriteString(c.w, `OFFSET ('`)
|
||||
io.WriteString(c.w, sel.Paging.Offset)
|
||||
io.WriteString(c.w, `') :: integer`)
|
||||
}
|
||||
|
||||
if !ti.Singular {
|
||||
//fmt.Fprintf(w, `) AS "json_agg_%d"`, c.sel.ID)
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "json_agg", sel.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderLateralJoin(sel *qcode.Select) error {
|
||||
io.WriteString(c.w, ` LEFT OUTER JOIN LATERAL (`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderLateralJoinClose(sel *qcode.Select) error {
|
||||
//fmt.Fprintf(w, `) AS "%s_%d_join" ON ('true')`, c.sel.Name, c.sel.ID)
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithIDSuffix(c.w, sel.Name, sel.ID, "_join")
|
||||
io.WriteString(c.w, `) `)
|
||||
aliasWithID(c.w, "__sel", sel.ID)
|
||||
io.WriteString(c.w, ` ON ('true')`)
|
||||
return nil
|
||||
}
|
||||
@ -460,7 +500,7 @@ func (c *compilerContext) renderJoinByName(table, parent string, id int32) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) {
|
||||
func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error {
|
||||
i := 0
|
||||
for _, col := range sel.Cols {
|
||||
n := funcPrefixLen(col.Name)
|
||||
@ -484,15 +524,21 @@ func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) {
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
|
||||
//c.sel.Name, c.sel.ID, col.Name, col.FieldName)
|
||||
colWithTableIDAlias(c.w, ti.Name, sel.ID, col.Name, col.FieldName)
|
||||
|
||||
squoted(c.w, col.FieldName)
|
||||
io.WriteString(c.w, ", ")
|
||||
colWithTableID(c.w, ti.Name, sel.ID, col.Name)
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
i += c.renderRemoteRelColumns(sel, ti, i)
|
||||
|
||||
return c.renderJoinColumns(sel, ti, skipped, i)
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo) {
|
||||
i := 0
|
||||
func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo, colsRendered int) int {
|
||||
i := colsRendered
|
||||
|
||||
for _, id := range sel.Children {
|
||||
child := &c.s[id]
|
||||
@ -504,18 +550,19 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableI
|
||||
if i != 0 || len(sel.Cols) != 0 {
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
|
||||
//c.sel.Name, c.sel.ID, rel.Left.Col, rel.Right.Col)
|
||||
|
||||
squoted(c.w, rel.Right.Col)
|
||||
io.WriteString(c.w, ", ")
|
||||
colWithTableID(c.w, ti.Name, sel.ID, rel.Left.Col)
|
||||
alias(c.w, rel.Right.Col)
|
||||
i++
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error {
|
||||
|
||||
func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32, colsRendered int) error {
|
||||
// columns previously rendered
|
||||
i := len(sel.Cols)
|
||||
i := colsRendered
|
||||
|
||||
for _, id := range sel.Children {
|
||||
if hasBit(skipped, uint32(id)) {
|
||||
@ -530,18 +577,20 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
|
||||
//s.Name, s.ID, s.Name, s.FieldName)
|
||||
//if cti.Singular {
|
||||
io.WriteString(c.w, `"`)
|
||||
io.WriteString(c.w, childSel.Name)
|
||||
io.WriteString(c.w, `_`)
|
||||
squoted(c.w, childSel.FieldName)
|
||||
|
||||
io.WriteString(c.w, `, "__sel_`)
|
||||
int2string(c.w, childSel.ID)
|
||||
io.WriteString(c.w, `_join"."json_`)
|
||||
int2string(c.w, childSel.ID)
|
||||
io.WriteString(c.w, `" AS "`)
|
||||
io.WriteString(c.w, `"."json"`)
|
||||
|
||||
if childSel.Paging.Type != qcode.PtOffset {
|
||||
io.WriteString(c.w, `, '`)
|
||||
io.WriteString(c.w, childSel.FieldName)
|
||||
io.WriteString(c.w, `"`)
|
||||
io.WriteString(c.w, `_cursor', "__sel_`)
|
||||
int2string(c.w, childSel.ID)
|
||||
io.WriteString(c.w, `"."cursor"`)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
@ -550,171 +599,29 @@ func (c *compilerContext) renderJoinedColumns(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 := (rel == nil)
|
||||
isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop)
|
||||
isSearch := sel.Args["search"] != nil
|
||||
isAgg := false
|
||||
hasOrder := len(sel.OrderBy) != 0
|
||||
|
||||
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]
|
||||
|
||||
if !isRealCol {
|
||||
if isSearch {
|
||||
switch {
|
||||
case cn == "search_rank":
|
||||
if len(sel.Allowed) != 0 {
|
||||
if _, ok := sel.Allowed[cn]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ti.TSVCol == nil {
|
||||
return errors.New("no ts_vector column found")
|
||||
}
|
||||
cn = ti.TSVCol.Name
|
||||
arg := sel.Args["search"]
|
||||
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
//fmt.Fprintf(w, `ts_rank("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
|
||||
//c.sel.Name, cn, arg.Val, col.Name)
|
||||
io.WriteString(c.w, `ts_rank(`)
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
if c.schema.ver >= 110000 {
|
||||
io.WriteString(c.w, `, websearch_to_tsquery('`)
|
||||
} else {
|
||||
io.WriteString(c.w, `, to_tsquery('`)
|
||||
}
|
||||
io.WriteString(c.w, arg.Val)
|
||||
io.WriteString(c.w, `'))`)
|
||||
alias(c.w, col.Name)
|
||||
i++
|
||||
|
||||
case strings.HasPrefix(cn, "search_headline_"):
|
||||
cn1 := cn[16:]
|
||||
if len(sel.Allowed) != 0 {
|
||||
if _, ok := sel.Allowed[cn1]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
arg := sel.Args["search"]
|
||||
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
//fmt.Fprintf(w, `ts_headline("%s"."%s", websearch_to_tsquery('%s')) AS %s`,
|
||||
//c.sel.Name, cn, arg.Val, col.Name)
|
||||
io.WriteString(c.w, `ts_headline(`)
|
||||
colWithTable(c.w, ti.Name, cn1)
|
||||
if c.schema.ver >= 110000 {
|
||||
io.WriteString(c.w, `, websearch_to_tsquery('`)
|
||||
} else {
|
||||
io.WriteString(c.w, `, to_tsquery('`)
|
||||
}
|
||||
io.WriteString(c.w, arg.Val)
|
||||
io.WriteString(c.w, `'))`)
|
||||
alias(c.w, col.Name)
|
||||
i++
|
||||
|
||||
}
|
||||
} else {
|
||||
pl := funcPrefixLen(cn)
|
||||
if pl == 0 {
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
//fmt.Fprintf(w, `'%s not defined' AS %s`, cn, col.Name)
|
||||
io.WriteString(c.w, `'`)
|
||||
io.WriteString(c.w, cn)
|
||||
io.WriteString(c.w, ` not defined'`)
|
||||
alias(c.w, col.Name)
|
||||
i++
|
||||
|
||||
} else if sel.Functions {
|
||||
cn1 := cn[pl:]
|
||||
if len(sel.Allowed) != 0 {
|
||||
if _, ok := sel.Allowed[cn1]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
fn := cn[0 : pl-1]
|
||||
isAgg = true
|
||||
|
||||
//fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Name, cn, col.Name)
|
||||
io.WriteString(c.w, fn)
|
||||
io.WriteString(c.w, `(`)
|
||||
colWithTable(c.w, ti.Name, cn1)
|
||||
io.WriteString(c.w, `)`)
|
||||
alias(c.w, col.Name)
|
||||
i++
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
groupBy = append(groupBy, n)
|
||||
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, cn)
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
i++
|
||||
|
||||
}
|
||||
if sel.Paging.Cursor {
|
||||
c.renderCursorCTE(sel)
|
||||
}
|
||||
|
||||
for _, ob := range sel.OrderBy {
|
||||
if _, ok := colmap[ob.Col]; ok {
|
||||
continue
|
||||
}
|
||||
colmap[ob.Col] = struct{}{}
|
||||
io.WriteString(c.w, `SELECT `)
|
||||
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
colWithTable(c.w, ti.Name, ob.Col)
|
||||
i++
|
||||
if len(sel.DistinctOn) != 0 {
|
||||
c.renderDistinctOn(sel, ti)
|
||||
}
|
||||
|
||||
for _, col := range childCols {
|
||||
if _, ok := colmap[col.Name]; ok {
|
||||
continue
|
||||
}
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, `"%s"."%s"`, col.Table, col.Name)
|
||||
colWithTable(c.w, col.Table, col.Name)
|
||||
i++
|
||||
realColsRendered, isAgg, err := c.renderBaseColumns(sel, ti, childCols, skipped)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.WriteString(c.w, ` FROM `)
|
||||
|
||||
c.renderFrom(sel, ti, rel)
|
||||
|
||||
// if tn, ok := c.tmap[sel.Name]; ok {
|
||||
// //fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Name)
|
||||
// tableWithAlias(c.w, ti.Name, sel.Name)
|
||||
// } else {
|
||||
// //fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name)
|
||||
// io.WriteString(c.w, `"`)
|
||||
// io.WriteString(c.w, sel.Name)
|
||||
// io.WriteString(c.w, `"`)
|
||||
// }
|
||||
|
||||
if isRoot && isFil {
|
||||
io.WriteString(c.w, ` WHERE (`)
|
||||
if err := c.renderWhere(sel, ti); err != nil {
|
||||
@ -741,18 +648,20 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
|
||||
io.WriteString(c.w, `)`)
|
||||
}
|
||||
|
||||
if isAgg {
|
||||
if len(groupBy) != 0 {
|
||||
if isAgg && len(realColsRendered) != 0 {
|
||||
io.WriteString(c.w, ` GROUP BY `)
|
||||
|
||||
for i, id := range groupBy {
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
for i, id := range realColsRendered {
|
||||
c.renderComma(i)
|
||||
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, c.sel.Cols[id].Name)
|
||||
colWithTable(c.w, ti.Name, sel.Cols[id].Name)
|
||||
}
|
||||
}
|
||||
|
||||
if hasOrder {
|
||||
if err := c.renderOrderBy(sel, ti); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
@ -779,10 +688,6 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
|
||||
io.WriteString(c.w, `') :: integer`)
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID)
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, ti.Name, sel.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -821,24 +726,26 @@ func (c *compilerContext) renderFrom(sel *qcode.Select, ti *DBTableInfo, rel *DB
|
||||
io.WriteString(c.w, `"`)
|
||||
}
|
||||
|
||||
if sel.Paging.Cursor {
|
||||
io.WriteString(c.w, `, "__cur"`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInfo) {
|
||||
//colsRendered := len(sel.Cols) != 0
|
||||
|
||||
for i := range sel.OrderBy {
|
||||
//io.WriteString(w, ", ")
|
||||
func (c *compilerContext) renderCursorCTE(sel *qcode.Select) error {
|
||||
io.WriteString(c.w, `WITH "__cur" AS (SELECT `)
|
||||
for i, ob := range sel.OrderBy {
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
|
||||
col := sel.OrderBy[i].Col
|
||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`,
|
||||
//c.sel.Name, c.sel.ID, c,
|
||||
//c.sel.Name, c.sel.ID, c)
|
||||
colWithTableID(c.w, ti.Name, sel.ID, col)
|
||||
io.WriteString(c.w, ` AS `)
|
||||
tableIDColSuffix(c.w, sel.Name, sel.ID, col, "_ob")
|
||||
}
|
||||
io.WriteString(c.w, `a[`)
|
||||
int2string(c.w, int32(i+1))
|
||||
io.WriteString(c.w, `] as `)
|
||||
quoted(c.w, ob.Col)
|
||||
}
|
||||
io.WriteString(c.w, ` FROM string_to_array('{{cursor}}', ',') as a) `)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
||||
@ -961,7 +868,6 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b
|
||||
switch val.Op {
|
||||
case qcode.OpFalse:
|
||||
st.Push(val.Op)
|
||||
qcode.FreeExp(val)
|
||||
|
||||
case qcode.OpAnd, qcode.OpOr:
|
||||
st.Push(')')
|
||||
@ -972,12 +878,12 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b
|
||||
}
|
||||
}
|
||||
st.Push('(')
|
||||
qcode.FreeExp(val)
|
||||
|
||||
case qcode.OpNot:
|
||||
//fmt.Printf("1> %s %d %s %s\n", val.Op, len(val.Children), val.Children[0].Op, val.Children[1].Op)
|
||||
|
||||
st.Push(val.Children[0])
|
||||
st.Push(qcode.OpNot)
|
||||
qcode.FreeExp(val)
|
||||
|
||||
default:
|
||||
if !skipNested && len(val.NestedCols) != 0 {
|
||||
@ -992,14 +898,13 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b
|
||||
if err := c.renderOp(val, ti); err != nil {
|
||||
return err
|
||||
}
|
||||
qcode.FreeExp(val)
|
||||
}
|
||||
}
|
||||
//qcode.FreeExp(val)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("12: unexpected value %v (%t)", intf, intf)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -1066,8 +971,12 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error {
|
||||
|
||||
switch ex.Op {
|
||||
case qcode.OpEquals:
|
||||
io.WriteString(c.w, `IS NOT DISTINCT FROM`)
|
||||
io.WriteString(c.w, `=`)
|
||||
case qcode.OpNotEquals:
|
||||
io.WriteString(c.w, `!=`)
|
||||
case qcode.OpNotDistinct:
|
||||
io.WriteString(c.w, `IS NOT DISTINCT FROM`)
|
||||
case qcode.OpDistinct:
|
||||
io.WriteString(c.w, `IS DISTINCT FROM`)
|
||||
case qcode.OpGreaterOrEquals:
|
||||
io.WriteString(c.w, `>=`)
|
||||
@ -1130,23 +1039,24 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error {
|
||||
io.WriteString(c.w, `((`)
|
||||
colWithTable(c.w, ti.Name, ti.TSVCol.Name)
|
||||
if c.schema.ver >= 110000 {
|
||||
io.WriteString(c.w, `) @@ websearch_to_tsquery('`)
|
||||
io.WriteString(c.w, `) @@ websearch_to_tsquery('{{`)
|
||||
} else {
|
||||
io.WriteString(c.w, `) @@ to_tsquery('`)
|
||||
io.WriteString(c.w, `) @@ to_tsquery('{{`)
|
||||
}
|
||||
io.WriteString(c.w, ex.Val)
|
||||
io.WriteString(c.w, `'))`)
|
||||
io.WriteString(c.w, `}}'))`)
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("[Where] unexpected op code %d", ex.Op)
|
||||
}
|
||||
|
||||
if ex.Type == qcode.ValList {
|
||||
switch {
|
||||
case ex.Type == qcode.ValList:
|
||||
c.renderList(ex)
|
||||
} else if col == nil {
|
||||
case col == nil:
|
||||
return errors.New("no column found for expression value")
|
||||
} else {
|
||||
default:
|
||||
c.renderVal(ex, c.vars, col)
|
||||
}
|
||||
|
||||
@ -1161,31 +1071,20 @@ func (c *compilerContext) renderOrderBy(sel *qcode.Select, ti *DBTableInfo) erro
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
ob := sel.OrderBy[i]
|
||||
colWithTable(c.w, ti.Name, ob.Col)
|
||||
|
||||
switch ob.Order {
|
||||
case qcode.OrderAsc:
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Name, sel.ID, ob.Col)
|
||||
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, 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, 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, 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, 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, sel.Name, sel.ID, ob.Col, "_ob")
|
||||
io.WriteString(c.w, ` DESC NULLS LAST`)
|
||||
default:
|
||||
return fmt.Errorf("13: unexpected value %v", ob.Order)
|
||||
@ -1200,8 +1099,7 @@ func (c *compilerContext) renderDistinctOn(sel *qcode.Select, ti *DBTableInfo) {
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s"`, c.sel.Name, c.sel.ID, c.sel.DistinctOn[i])
|
||||
tableIDColSuffix(c.w, ti.Name, sel.ID, sel.DistinctOn[i], "_ob")
|
||||
colWithTable(c.w, ti.Name, sel.DistinctOn[i])
|
||||
}
|
||||
io.WriteString(c.w, `) `)
|
||||
}
|
||||
@ -1228,32 +1126,30 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *
|
||||
io.WriteString(c.w, ` `)
|
||||
|
||||
switch ex.Type {
|
||||
case qcode.ValBool, qcode.ValInt, qcode.ValFloat:
|
||||
if len(ex.Val) != 0 {
|
||||
io.WriteString(c.w, ex.Val)
|
||||
} else {
|
||||
io.WriteString(c.w, `''`)
|
||||
}
|
||||
|
||||
case qcode.ValStr:
|
||||
io.WriteString(c.w, `'`)
|
||||
io.WriteString(c.w, ex.Val)
|
||||
io.WriteString(c.w, `'`)
|
||||
|
||||
case qcode.ValVar:
|
||||
io.WriteString(c.w, `'`)
|
||||
if val, ok := vars[ex.Val]; ok {
|
||||
io.WriteString(c.w, val)
|
||||
} else {
|
||||
//fmt.Fprintf(w, `'{{%s}}'`, ex.Val)
|
||||
io.WriteString(c.w, `{{`)
|
||||
val, ok := vars[ex.Val]
|
||||
switch {
|
||||
case ok && strings.HasPrefix(val, "sql:"):
|
||||
io.WriteString(c.w, ` (`)
|
||||
io.WriteString(c.w, val[4:])
|
||||
io.WriteString(c.w, `)`)
|
||||
case ok:
|
||||
squoted(c.w, val)
|
||||
default:
|
||||
io.WriteString(c.w, ` '{{`)
|
||||
io.WriteString(c.w, ex.Val)
|
||||
io.WriteString(c.w, `}}`)
|
||||
io.WriteString(c.w, `}}'`)
|
||||
}
|
||||
io.WriteString(c.w, `' :: `)
|
||||
|
||||
case qcode.ValRef:
|
||||
colWithTable(c.w, ex.Table, ex.Col)
|
||||
|
||||
default:
|
||||
squoted(c.w, ex.Val)
|
||||
}
|
||||
|
||||
io.WriteString(c.w, ` :: `)
|
||||
io.WriteString(c.w, col.Type)
|
||||
}
|
||||
//io.WriteString(c.w, `)`)
|
||||
}
|
||||
|
||||
func funcPrefixLen(fn string) int {
|
||||
@ -1303,15 +1199,6 @@ func aliasWithID(w io.Writer, alias string, id int32) {
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func aliasWithIDSuffix(w io.Writer, alias string, id int32, suffix string) {
|
||||
io.WriteString(w, ` AS "`)
|
||||
io.WriteString(w, alias)
|
||||
io.WriteString(w, `_`)
|
||||
int2string(w, id)
|
||||
io.WriteString(w, suffix)
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func colWithTable(w io.Writer, table, col string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, table)
|
||||
@ -1332,27 +1219,16 @@ func colWithTableID(w io.Writer, table string, id int32, col string) {
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func colWithTableIDAlias(w io.Writer, table string, id int32, col, alias string) {
|
||||
func quoted(w io.Writer, identifier string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, table)
|
||||
io.WriteString(w, `_`)
|
||||
int2string(w, id)
|
||||
io.WriteString(w, `"."`)
|
||||
io.WriteString(w, col)
|
||||
io.WriteString(w, `" AS "`)
|
||||
io.WriteString(w, alias)
|
||||
io.WriteString(w, identifier)
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func tableIDColSuffix(w io.Writer, table string, id int32, col, suffix string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, table)
|
||||
io.WriteString(w, `_`)
|
||||
int2string(w, id)
|
||||
io.WriteString(w, `_`)
|
||||
io.WriteString(w, col)
|
||||
io.WriteString(w, suffix)
|
||||
io.WriteString(w, `"`)
|
||||
func squoted(w io.Writer, identifier string) {
|
||||
io.WriteString(w, `'`)
|
||||
io.WriteString(w, identifier)
|
||||
io.WriteString(w, `'`)
|
||||
}
|
||||
|
||||
const charset = "0123456789"
|
||||
|
@ -2,6 +2,7 @@ package psql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -28,16 +29,41 @@ func withComplexArgs(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0" ORDER BY "products_0_price_ob" DESC), '[]') AS "json_0" FROM (SELECT DISTINCT ON ("products_0_price_ob") row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0", "products_0"."price" AS "products_0_price_ob" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."id") < 28) AND (("products"."id") >= 20)))) LIMIT ('30') :: integer) AS "products_0" ORDER BY "products_0_price_ob" DESC LIMIT ('30') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
func withWhereAndList(t *testing.T) {
|
||||
gql := `query {
|
||||
products(
|
||||
where: {
|
||||
and: [
|
||||
{ not: { id: { is_null: true } } },
|
||||
{ price: { gt: 10 } },
|
||||
] } ) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}`
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func withWhereIsNull(t *testing.T) {
|
||||
gql := `query {
|
||||
products(
|
||||
where: {
|
||||
and: {
|
||||
not: { id: { is_null: true } },
|
||||
price: { gt: 10 }
|
||||
}}) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}`
|
||||
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func withWhereMultiOr(t *testing.T) {
|
||||
@ -56,93 +82,23 @@ func withWhereMultiOr(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") < 20) OR (("products"."price") > 10) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
|
||||
func withWhereIsNull(t *testing.T) {
|
||||
gql := `query {
|
||||
products(
|
||||
where: {
|
||||
and: {
|
||||
not: { id: { is_null: true } },
|
||||
price: { gt: 10 }
|
||||
}}) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}`
|
||||
|
||||
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", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") > 10) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
|
||||
func withWhereAndList(t *testing.T) {
|
||||
gql := `query {
|
||||
products(
|
||||
where: {
|
||||
and: [
|
||||
{ not: { id: { is_null: true } } },
|
||||
{ price: { gt: 10 } },
|
||||
] } ) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}`
|
||||
|
||||
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", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") > 10) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func fetchByID(t *testing.T) {
|
||||
gql := `query {
|
||||
product(id: 15) {
|
||||
product(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `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" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") = 15))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func searchQuery(t *testing.T) {
|
||||
gql := `query {
|
||||
products(search: "ale") {
|
||||
products(search: $query) {
|
||||
id
|
||||
name
|
||||
search_rank
|
||||
@ -150,16 +106,7 @@ func searchQuery(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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", "products_0"."search_rank" AS "search_rank", "products_0"."search_headline_description" AS "search_headline_description") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('ale')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('ale')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('ale'))) LIMIT ('20') :: integer) AS "products_0" 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)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "admin")
|
||||
}
|
||||
|
||||
func oneToMany(t *testing.T) {
|
||||
@ -173,16 +120,7 @@ func oneToMany(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND ((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func oneToManyReverse(t *testing.T) {
|
||||
@ -196,16 +134,7 @@ func oneToManyReverse(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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"."name" AS "name", "products_0"."price" AS "price", "users_1_join"."json_1" AS "users") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "users_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func oneToManyArray(t *testing.T) {
|
||||
@ -227,16 +156,7 @@ func oneToManyArray(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT row_to_json("json_root") FROM (SELECT "sel_0"."json_0" AS "tags", "sel_2"."json_2" AS "product" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "products_2"."name" AS "name", "products_2"."price" AS "price", "tags_3_join"."json_3" AS "tags") AS "json_row_2")) AS "json_2" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_3"), '[]') AS "json_3" FROM (SELECT row_to_json((SELECT "json_row_3" FROM (SELECT "tags_3"."id" AS "id", "tags_3"."name" AS "name") AS "json_row_3")) AS "json_3" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3" LIMIT ('20') :: integer) AS "json_agg_3") AS "tags_3_join" ON ('true') LIMIT ('1') :: integer) AS "sel_2", (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "tags_0"."name" AS "name", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0") AS "json_root"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "admin")
|
||||
}
|
||||
|
||||
func manyToMany(t *testing.T) {
|
||||
@ -250,16 +170,7 @@ func manyToMany(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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"."name" AS "name", "customers_1_join"."json_1" AS "customers") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "json_row_1")) AS "json_1" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func manyToManyReverse(t *testing.T) {
|
||||
@ -273,16 +184,7 @@ func manyToManyReverse(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('customers', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func aggFunction(t *testing.T) {
|
||||
@ -293,16 +195,7 @@ func aggFunction(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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"."name" AS "name", "products_0"."count_price" AS "count_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func aggFunctionBlockedByCol(t *testing.T) {
|
||||
@ -313,16 +206,7 @@ func aggFunctionBlockedByCol(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" 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)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "anon")
|
||||
}
|
||||
|
||||
func aggFunctionDisabled(t *testing.T) {
|
||||
@ -333,16 +217,7 @@ func aggFunctionDisabled(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "anon1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "anon1")
|
||||
}
|
||||
|
||||
func aggFunctionWithFilter(t *testing.T) {
|
||||
@ -353,16 +228,7 @@ func aggFunctionWithFilter(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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"."max_price" AS "max_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", max("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") > 10))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func syntheticTables(t *testing.T) {
|
||||
@ -372,16 +238,7 @@ func syntheticTables(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('me', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT ) AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func queryWithVariables(t *testing.T) {
|
||||
@ -392,16 +249,7 @@ func queryWithVariables(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `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" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") IS NOT DISTINCT FROM '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint)))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func withWhereOnRelations(t *testing.T) {
|
||||
@ -418,16 +266,7 @@ func withWhereOnRelations(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > 3)))) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
func multiRoot(t *testing.T) {
|
||||
@ -451,16 +290,24 @@ func multiRoot(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT row_to_json("json_root") FROM (SELECT "sel_0"."json_0" AS "customer", "sel_1"."json_1" AS "user", "sel_2"."json_2" AS "product" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "products_2"."id" AS "id", "products_2"."name" AS "name", "customers_3_join"."json_3" AS "customers", "customer_4_join"."json_4" AS "customer") AS "json_row_2")) AS "json_2" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_4" FROM (SELECT "customers_4"."email" AS "email") AS "json_row_4")) AS "json_4" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4" LIMIT ('1') :: integer) AS "customer_4_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_3"), '[]') AS "json_3" FROM (SELECT row_to_json((SELECT "json_row_3" FROM (SELECT "customers_3"."email" AS "email") AS "json_row_3")) AS "json_3" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3" LIMIT ('20') :: integer) AS "json_agg_3") AS "customers_3_join" ON ('true') LIMIT ('1') :: integer) AS "sel_2", (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "sel_1", (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0" LIMIT ('1') :: integer) AS "sel_0") AS "json_root"`
|
||||
compileGQLToPSQL(t, gql, nil, "user")
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
func withCursor(t *testing.T) {
|
||||
gql := `query {
|
||||
Products(
|
||||
first: 20
|
||||
after: $cursor
|
||||
order_by: { price: desc }) {
|
||||
Name
|
||||
}
|
||||
}`
|
||||
|
||||
vars := map[string]json.RawMessage{
|
||||
"cursor": json.RawMessage(`"0,1"`),
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func jsonColumnAsTable(t *testing.T) {
|
||||
@ -477,16 +324,7 @@ func jsonColumnAsTable(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "admin")
|
||||
}
|
||||
|
||||
func skipUserIDForAnonRole(t *testing.T) {
|
||||
@ -501,37 +339,19 @@ func skipUserIDForAnonRole(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
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)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "anon")
|
||||
}
|
||||
|
||||
func blockedQuery(t *testing.T) {
|
||||
gql := `query {
|
||||
user(id: 5, where: { id: { gt: 3 } }) {
|
||||
user(id: $id, where: { id: { gt: 3 } }) {
|
||||
id
|
||||
full_name
|
||||
email
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `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") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "bad_dude")
|
||||
}
|
||||
|
||||
func blockedFunctions(t *testing.T) {
|
||||
@ -542,16 +362,7 @@ func blockedFunctions(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE (false) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, nil, "bad_dude")
|
||||
}
|
||||
|
||||
func TestCompileQuery(t *testing.T) {
|
||||
@ -575,6 +386,7 @@ func TestCompileQuery(t *testing.T) {
|
||||
t.Run("withWhereOnRelations", withWhereOnRelations)
|
||||
t.Run("multiRoot", multiRoot)
|
||||
t.Run("jsonColumnAsTable", jsonColumnAsTable)
|
||||
t.Run("withCursor", withCursor)
|
||||
t.Run("skipUserIDForAnonRole", skipUserIDForAnonRole)
|
||||
t.Run("blockedQuery", blockedQuery)
|
||||
t.Run("blockedFunctions", blockedFunctions)
|
||||
|
@ -151,6 +151,7 @@ SELECT
|
||||
pg_catalog.format_type(f.atttypid,f.atttypmod) AS type,
|
||||
CASE
|
||||
WHEN f.attndims != 0 THEN true
|
||||
WHEN right(pg_catalog.format_type(f.atttypid,f.atttypmod), 2) = '[]' THEN true
|
||||
ELSE false
|
||||
END AS array,
|
||||
CASE
|
||||
@ -166,7 +167,7 @@ SELECT
|
||||
ELSE ''::text
|
||||
END AS foreignkey,
|
||||
CASE
|
||||
WHEN p.contype = ('f'::char) THEN p.confkey
|
||||
WHEN p.contype = ('f'::char) THEN p.confkey::int2[]
|
||||
ELSE ARRAY[]::int2[]
|
||||
END AS foreignkey_fieldnum
|
||||
FROM pg_attribute f
|
||||
@ -175,7 +176,7 @@ FROM pg_attribute f
|
||||
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
|
||||
LEFT JOIN pg_class AS g ON p.confrelid = g.oid
|
||||
WHERE c.relkind = ('r'::char)
|
||||
WHERE c.relkind IN ('r', 'v', 'm', 'f')
|
||||
AND n.nspname = $1 -- Replace with Schema name
|
||||
AND c.relname = $2 -- Replace with table name
|
||||
AND f.attnum > 0
|
||||
@ -243,3 +244,13 @@ ORDER BY id;`
|
||||
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
// func GetValType(type string) qcode.ValType {
|
||||
// switch {
|
||||
// case "bigint", "integer", "smallint", "numeric", "bigserial":
|
||||
// return qcode.ValInt
|
||||
// case "double precision", "real":
|
||||
// return qcode.ValFloat
|
||||
// case ""
|
||||
// }
|
||||
// }
|
||||
|
151
psql/tests.sql
Normal file
151
psql/tests.sql
Normal file
@ -0,0 +1,151 @@
|
||||
=== RUN TestCompileInsert
|
||||
=== RUN TestCompileInsert/simpleInsert
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id") AS "json" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0") AS "__sel_0"
|
||||
=== RUN TestCompileInsert/singleInsert
|
||||
WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT "t"."name", "t"."description", "t"."price", "t"."user_id" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileInsert/bulkInsert
|
||||
WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileInsert/simpleInsertWithPresets
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id") AS "json" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileInsert/nestedInsertManyToMany
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products", json_populate_record(NULL::purchases, i.j) t RETURNING *) SELECT json_build_object('purchase', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "__sel_1"."json", 'customer', "__sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "product_id", "customer_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "products"."id", "customers"."id" FROM "_sg_input" i, "products", "customers", json_populate_record(NULL::purchases, i.j) t RETURNING *) SELECT json_build_object('purchase', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "__sel_1"."json", 'customer', "__sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToMany
|
||||
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 (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->'product') t RETURNING *) SELECT json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "__sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToOne
|
||||
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->'user') t RETURNING *), "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_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToManyWithConnect
|
||||
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_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "__sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToOneWithConnect
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json", 'tags', "__sel_2"."json") AS "json" 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("__sel_2"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "tags_2"."id", 'name', "tags_2"."name") AS "json" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sel_2") AS "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToOneWithConnectArray
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id" = ANY((select a::bigint AS list from json_array_elements_text((i.j->'user'->'connect'->>'id')::json) AS a)) LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
--- PASS: TestCompileInsert (0.02s)
|
||||
--- PASS: TestCompileInsert/simpleInsert (0.00s)
|
||||
--- PASS: TestCompileInsert/singleInsert (0.00s)
|
||||
--- PASS: TestCompileInsert/bulkInsert (0.00s)
|
||||
--- PASS: TestCompileInsert/simpleInsertWithPresets (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertManyToMany (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToMany (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToOne (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToManyWithConnect (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToOneWithConnect (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToOneWithConnectArray (0.00s)
|
||||
=== RUN TestCompileMutate
|
||||
=== RUN TestCompileMutate/singleUpsert
|
||||
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileMutate/singleUpsertWhere
|
||||
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) WHERE (("products"."price") > '3' :: numeric(7,2)) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileMutate/bulkUpsert
|
||||
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileMutate/delete
|
||||
WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '1' :: bigint)) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
--- PASS: TestCompileMutate (0.01s)
|
||||
--- PASS: TestCompileMutate/singleUpsert (0.00s)
|
||||
--- PASS: TestCompileMutate/singleUpsertWhere (0.00s)
|
||||
--- PASS: TestCompileMutate/bulkUpsert (0.00s)
|
||||
--- PASS: TestCompileMutate/delete (0.00s)
|
||||
=== RUN TestCompileQuery
|
||||
=== RUN TestCompileQuery/withComplexArgs
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price") AS "json" FROM (SELECT DISTINCT ON ("products"."price") "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."id") < '28' :: bigint) AND (("products"."id") >= '20' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) ORDER BY "products"."price" DESC LIMIT ('30') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/withWhereAndList
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/withWhereIsNull
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/withWhereMultiOr
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") < '20' :: numeric(7,2)) OR (("products"."price") > '10' :: numeric(7,2)) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/fetchByID
|
||||
SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '{{id}}' :: bigint))) LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/searchQuery
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'search_rank', "products_0"."search_rank", 'search_headline_description', "products_0"."search_headline_description") AS "json" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('{{query}}')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('{{query}}')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('{{query}}'))) LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/oneToMany
|
||||
SELECT json_build_object('users', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "users_0"."email", 'products', "__sel_1"."json") AS "json" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_1"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sel_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/oneToManyReverse
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name", 'price', "products_0"."price", 'users', "__sel_1"."json") AS "json" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_1"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "users_1"."email") AS "json" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1") AS "__sel_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/oneToManyArray
|
||||
SELECT json_build_object('tags', "__sel_0"."json", 'product', "__sel_2"."json") as "__root" FROM (SELECT json_build_object('name', "products_2"."name", 'price', "products_2"."price", 'tags', "__sel_3"."json") AS "json" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_3"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "tags_3"."id", 'name', "tags_3"."name") AS "json" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3") AS "__sel_3") AS "__sel_3" ON ('true')) AS "__sel_2", (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "tags_0"."name", 'product', "__sel_1"."json") AS "json" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('name', "products_1"."name") AS "json" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/manyToMany
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name", 'customers', "__sel_1"."json") AS "json" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_1"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "customers_1"."email", 'full_name', "customers_1"."full_name") AS "json" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1") AS "__sel_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/manyToManyReverse
|
||||
SELECT json_build_object('customers', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "customers_0"."email", 'full_name', "customers_0"."full_name", 'products', "__sel_1"."json") AS "json" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_1"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_1"."name") AS "json" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sel_1") AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/aggFunction
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name", 'count_price', "products_0"."count_price") AS "json" FROM (SELECT "products"."name", price("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/aggFunctionBlockedByCol
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name") AS "json" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/aggFunctionDisabled
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "products_0"."name") AS "json" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/aggFunctionWithFilter
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'max_price', "products_0"."max_price") AS "json" FROM (SELECT "products"."id", pri("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") > '10' :: bigint))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/syntheticTables
|
||||
SELECT json_build_object('me', "__sel_0"."json") as "__root" FROM (SELECT json_build_object() AS "json" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/queryWithVariables
|
||||
SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") = '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/withWhereOnRelations
|
||||
SELECT json_build_object('users', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "users_0"."id", 'email', "users_0"."email") AS "json" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > '3' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "users_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/multiRoot
|
||||
SELECT json_build_object('customer', "__sel_0"."json", 'user', "__sel_1"."json", 'product', "__sel_2"."json") as "__root" FROM (SELECT json_build_object('id', "products_2"."id", 'name', "products_2"."name", 'customers', "__sel_3"."json", 'customer', "__sel_4"."json") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT json_build_object('email', "customers_4"."email") AS "json" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4") AS "__sel_4" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("__sel_3"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "customers_3"."email") AS "json" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3") AS "__sel_3") AS "__sel_3" ON ('true')) AS "__sel_2", (SELECT json_build_object('id', "users_1"."id", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1") AS "__sel_1", (SELECT json_build_object('id', "customers_0"."id") AS "json" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/jsonColumnAsTable
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'tag_count', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('count', "tag_count_1"."count", 'tags', "__sel_2"."json") AS "json" 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("__sel_2"."json"), '[]') as "json" FROM (SELECT json_build_object('name', "tags_2"."name") AS "json" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sel_2") AS "__sel_2" ON ('true')) AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/withCursor
|
||||
SELECT json_build_object('products', "__sel_0"."json", 'products_cursor', "__sel_0"."cursor") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json", CONCAT_WS(',', max("__cur_0"), max("__cur_1")) as "cursor" FROM (SELECT json_build_object('name', "products_0"."name") AS "json", LAST_VALUE("products_0"."price") OVER() AS "__cur_0", LAST_VALUE("products_0"."id") OVER() AS "__cur_1" FROM (WITH "__cur" AS (SELECT a[1] as "price", a[2] as "id" FROM string_to_array('{{cursor}}', ',') as a) SELECT "products"."name", "products"."id", "products"."price" FROM "products", "__cur" WHERE (((("products"."price") < "__cur"."price" :: numeric(7,2)) OR ((("products"."price") = "__cur"."price" :: numeric(7,2)) AND (("products"."id") > "__cur"."id" :: bigint)))) ORDER BY "products"."price" DESC, "products"."id" ASC LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/skipUserIDForAnonRole
|
||||
SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/blockedQuery
|
||||
SELECT json_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0") AS "__sel_0"
|
||||
=== RUN TestCompileQuery/blockedFunctions
|
||||
SELECT json_build_object('users', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT json_build_object('email', "users_0"."email") AS "json" FROM (SELECT , "users"."email" FROM "users" WHERE (false) GROUP BY "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "__sel_0") AS "__sel_0"
|
||||
--- PASS: TestCompileQuery (0.02s)
|
||||
--- PASS: TestCompileQuery/withComplexArgs (0.00s)
|
||||
--- PASS: TestCompileQuery/withWhereAndList (0.00s)
|
||||
--- PASS: TestCompileQuery/withWhereIsNull (0.00s)
|
||||
--- PASS: TestCompileQuery/withWhereMultiOr (0.00s)
|
||||
--- PASS: TestCompileQuery/fetchByID (0.00s)
|
||||
--- PASS: TestCompileQuery/searchQuery (0.00s)
|
||||
--- PASS: TestCompileQuery/oneToMany (0.00s)
|
||||
--- PASS: TestCompileQuery/oneToManyReverse (0.00s)
|
||||
--- PASS: TestCompileQuery/oneToManyArray (0.00s)
|
||||
--- PASS: TestCompileQuery/manyToMany (0.00s)
|
||||
--- PASS: TestCompileQuery/manyToManyReverse (0.00s)
|
||||
--- PASS: TestCompileQuery/aggFunction (0.00s)
|
||||
--- PASS: TestCompileQuery/aggFunctionBlockedByCol (0.00s)
|
||||
--- PASS: TestCompileQuery/aggFunctionDisabled (0.00s)
|
||||
--- PASS: TestCompileQuery/aggFunctionWithFilter (0.00s)
|
||||
--- PASS: TestCompileQuery/syntheticTables (0.00s)
|
||||
--- PASS: TestCompileQuery/queryWithVariables (0.00s)
|
||||
--- PASS: TestCompileQuery/withWhereOnRelations (0.00s)
|
||||
--- PASS: TestCompileQuery/multiRoot (0.00s)
|
||||
--- PASS: TestCompileQuery/jsonColumnAsTable (0.00s)
|
||||
--- PASS: TestCompileQuery/withCursor (0.00s)
|
||||
--- PASS: TestCompileQuery/skipUserIDForAnonRole (0.00s)
|
||||
--- PASS: TestCompileQuery/blockedQuery (0.00s)
|
||||
--- PASS: TestCompileQuery/blockedFunctions (0.00s)
|
||||
=== RUN TestCompileUpdate
|
||||
=== RUN TestCompileUpdate/singleUpdate
|
||||
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") = '1' :: bigint) AND (("products"."id") = '{{id}}' :: bigint)) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileUpdate/simpleUpdateWithPresets
|
||||
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") = '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id") AS "json" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateManyToMany
|
||||
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") = '{{id}}' :: bigint) 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_build_object('purchase', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "__sel_1"."json", 'customer', "__sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
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") = '{{id}}' :: bigint) 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_build_object('purchase', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "__sel_1"."json", 'customer', "__sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToMany
|
||||
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") = '8' :: bigint) 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_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "__sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToOne
|
||||
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") = '{{id}}' :: bigint) 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_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToManyWithConnect
|
||||
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") = '{{id}}' :: bigint) 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_build_object('user', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "__sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToOneWithConnect
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint AND "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{product_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying AND "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{product_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "__sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sel_1" ON ('true')) AS "__sel_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToOneWithDisconnect
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '{{id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "__sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user_id', "products_0"."user_id") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sel_0"
|
||||
--- PASS: TestCompileUpdate (0.02s)
|
||||
--- PASS: TestCompileUpdate/singleUpdate (0.00s)
|
||||
--- PASS: TestCompileUpdate/simpleUpdateWithPresets (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateManyToMany (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToMany (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToOne (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToManyWithConnect (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToOneWithConnect (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToOneWithDisconnect (0.00s)
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/psql 0.716s
|
@ -228,6 +228,6 @@ func (c *compilerContext) renderDelete(qc *qcode.QCode, w io.Writer,
|
||||
|
||||
io.WriteString(w, ` RETURNING `)
|
||||
quoted(w, ti.Name)
|
||||
io.WriteString(w, `.*)`)
|
||||
io.WriteString(w, `.*) `)
|
||||
return 0, nil
|
||||
}
|
||||
|
@ -7,26 +7,17 @@ import (
|
||||
|
||||
func singleUpdate(t *testing.T) {
|
||||
gql := `mutation {
|
||||
product(id: 15, update: $update, where: { id: { eq: 1 } }) {
|
||||
product(id: $id, update: $update, where: { id: { eq: 1 } }) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`
|
||||
|
||||
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" }`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "anon")
|
||||
}
|
||||
|
||||
func simpleUpdateWithPresets(t *testing.T) {
|
||||
@ -36,25 +27,16 @@ 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"`
|
||||
|
||||
vars := map[string]json.RawMessage{
|
||||
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "user")
|
||||
}
|
||||
|
||||
func nestedUpdateManyToMany(t *testing.T) {
|
||||
gql := `mutation {
|
||||
purchase(update: $data, id: 5) {
|
||||
purchase(update: $data, id: $id) {
|
||||
sale_type
|
||||
quantity
|
||||
due_date
|
||||
@ -71,10 +53,6 @@ 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"`
|
||||
|
||||
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(` {
|
||||
"sale_type": "bought",
|
||||
@ -92,17 +70,7 @@ func nestedUpdateManyToMany(t *testing.T) {
|
||||
`),
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql1 && string(resSQL) != sql2 {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedUpdateOneToMany(t *testing.T) {
|
||||
@ -119,8 +87,6 @@ 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"= ((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(`{
|
||||
"email": "thedude@rug.com",
|
||||
@ -139,19 +105,12 @@ func nestedUpdateOneToMany(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedUpdateOneToOne(t *testing.T) {
|
||||
gql := `mutation {
|
||||
product(update: $data, id: 6) {
|
||||
product(update: $data, id: $id) {
|
||||
id
|
||||
name
|
||||
user {
|
||||
@ -162,8 +121,6 @@ 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"`
|
||||
|
||||
vars := map[string]json.RawMessage{
|
||||
"data": json.RawMessage(`{
|
||||
"name": "Apple",
|
||||
@ -176,19 +133,13 @@ func nestedUpdateOneToOne(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
|
||||
func nestedUpdateOneToManyWithConnect(t *testing.T) {
|
||||
gql := `mutation {
|
||||
user(update: $data, id: 6) {
|
||||
user(update: $data, id: $id) {
|
||||
id
|
||||
full_name
|
||||
email
|
||||
@ -200,8 +151,6 @@ 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"= ((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(`{
|
||||
"email": "thedude@rug.com",
|
||||
@ -215,19 +164,12 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql1 {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
||||
gql := `mutation {
|
||||
product(update: $data, id: 9) {
|
||||
product(update: $data, id: $product_id) {
|
||||
id
|
||||
name
|
||||
user {
|
||||
@ -238,10 +180,6 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint AND "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying AND "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
vars := map[string]json.RawMessage{
|
||||
"data": json.RawMessage(`{
|
||||
"name": "Apple",
|
||||
@ -252,29 +190,17 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql1 && string(resSQL) != sql2 {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
|
||||
gql := `mutation {
|
||||
product(update: $data, id: 2) {
|
||||
product(update: $data, id: $id) {
|
||||
id
|
||||
name
|
||||
user_id
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 2) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
|
||||
|
||||
vars := map[string]json.RawMessage{
|
||||
"data": json.RawMessage(`{
|
||||
"name": "Apple",
|
||||
@ -285,14 +211,7 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) {
|
||||
}`),
|
||||
}
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
compileGQLToPSQL(t, gql, vars, "admin")
|
||||
}
|
||||
|
||||
// func nestedUpdateOneToOneWithDisconnectArray(t *testing.T) {
|
||||
|
@ -556,6 +556,31 @@ func (t parserType) String() string {
|
||||
return fmt.Sprintf("<%s>", v)
|
||||
}
|
||||
|
||||
func FreeNode(n *Node) {
|
||||
// type Frees struct {
|
||||
// n *Node
|
||||
// loc int
|
||||
// }
|
||||
|
||||
// var freeList []Frees
|
||||
|
||||
// func FreeNode(n *Node, loc int) {
|
||||
// j := -1
|
||||
|
||||
// for i := range freeList {
|
||||
// if n == freeList[i].n {
|
||||
// j = i
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// if j == -1 {
|
||||
// nodePool.Put(n)
|
||||
// freeList = append(freeList, Frees{n, loc})
|
||||
// } else {
|
||||
// fmt.Printf(">>>>(%d) RE_FREE %d %p %s %s\n", loc, freeList[j].loc, freeList[j].n, n.Name, n.Type)
|
||||
// }
|
||||
// }
|
||||
|
||||
func FreeNode(n *Node, loc int) {
|
||||
nodePool.Put(n)
|
||||
}
|
||||
|
@ -22,8 +22,8 @@ func TestCompile1(t *testing.T) {
|
||||
name
|
||||
} }`), "user")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if err == nil {
|
||||
t.Fatal(errors.New("this should be an error id must be a variable"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ func TestCompile2(t *testing.T) {
|
||||
}
|
||||
|
||||
_, err = qc.Compile([]byte(`
|
||||
query { product(id: 15) {
|
||||
query { product(id: $id) {
|
||||
id
|
||||
name
|
||||
} }`), "user")
|
||||
@ -62,7 +62,7 @@ func TestCompile3(t *testing.T) {
|
||||
|
||||
_, err = qc.Compile([]byte(`
|
||||
mutation {
|
||||
product(id: 15, name: "Test") {
|
||||
product(id: $test, name: "Test") {
|
||||
id
|
||||
name
|
||||
}
|
||||
|
235
qcode/qcode.go
235
qcode/qcode.go
@ -65,6 +65,7 @@ type Exp struct {
|
||||
Col string
|
||||
NestedCols []string
|
||||
Type ValType
|
||||
Table string
|
||||
Val string
|
||||
ListType ValType
|
||||
ListVal []string
|
||||
@ -84,9 +85,19 @@ type OrderBy struct {
|
||||
Order Order
|
||||
}
|
||||
|
||||
type PagingType int
|
||||
|
||||
const (
|
||||
PtOffset PagingType = iota
|
||||
PtForward
|
||||
PtBackward
|
||||
)
|
||||
|
||||
type Paging struct {
|
||||
Type PagingType
|
||||
Limit string
|
||||
Offset string
|
||||
Cursor bool
|
||||
NoLimit bool
|
||||
}
|
||||
|
||||
@ -120,6 +131,8 @@ const (
|
||||
OpEqID
|
||||
OpTsQuery
|
||||
OpFalse
|
||||
OpNotDistinct
|
||||
OpDistinct
|
||||
)
|
||||
|
||||
type ValType int
|
||||
@ -132,6 +145,7 @@ const (
|
||||
ValList
|
||||
ValVar
|
||||
ValNone
|
||||
ValRef
|
||||
)
|
||||
|
||||
type AggregrateOp int
|
||||
@ -183,6 +197,13 @@ func NewCompiler(c Config) (*Compiler, error) {
|
||||
return co, nil
|
||||
}
|
||||
|
||||
func NewFilter() *Exp {
|
||||
ex := expPool.Get().(*Exp)
|
||||
ex.Reset()
|
||||
|
||||
return ex
|
||||
}
|
||||
|
||||
func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
||||
var err error
|
||||
trv := &trval{}
|
||||
@ -343,8 +364,8 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Order is important addFilters must come after compileArgs
|
||||
com.addFilters(qc, s, role)
|
||||
// Order is important AddFilters must come after compileArgs
|
||||
com.AddFilters(qc, s, role)
|
||||
|
||||
if s.ParentID == -1 {
|
||||
qc.Roots = append(qc.Roots, s.ID)
|
||||
@ -390,7 +411,7 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
||||
func (com *Compiler) AddFilters(qc *QCode, sel *Select, role string) {
|
||||
var fil *Exp
|
||||
var nu bool
|
||||
|
||||
@ -400,10 +421,6 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
||||
} else if role == "anon" {
|
||||
// Tables not defined under the anon role will not be rendered
|
||||
sel.SkipRender = true
|
||||
return
|
||||
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if fil == nil {
|
||||
@ -418,55 +435,58 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) {
|
||||
case OpNop:
|
||||
case OpFalse:
|
||||
sel.Where = fil
|
||||
|
||||
default:
|
||||
if sel.Where != nil {
|
||||
ow := sel.Where
|
||||
|
||||
sel.Where = expPool.Get().(*Exp)
|
||||
sel.Where.Reset()
|
||||
sel.Where.Op = OpAnd
|
||||
sel.Where.Children = sel.Where.childrenA[:2]
|
||||
sel.Where.Children[0] = fil
|
||||
sel.Where.Children[1] = ow
|
||||
} else {
|
||||
sel.Where = fil
|
||||
}
|
||||
AddFilter(sel, fil)
|
||||
}
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg, role string) error {
|
||||
var err error
|
||||
var ka bool
|
||||
|
||||
// don't free this arg either previously done or will be free'd
|
||||
// in the future like in psql
|
||||
var df bool
|
||||
|
||||
for i := range args {
|
||||
arg := &args[i]
|
||||
|
||||
switch arg.Name {
|
||||
case "id":
|
||||
err, ka = com.compileArgID(sel, arg)
|
||||
err, df = com.compileArgID(sel, arg)
|
||||
|
||||
case "search":
|
||||
err, ka = com.compileArgSearch(sel, arg)
|
||||
err, df = com.compileArgSearch(sel, arg)
|
||||
|
||||
case "where":
|
||||
err, ka = com.compileArgWhere(sel, arg, role)
|
||||
err, df = com.compileArgWhere(sel, arg, role)
|
||||
|
||||
case "orderby", "order_by", "order":
|
||||
err, ka = com.compileArgOrderBy(sel, arg)
|
||||
err, df = com.compileArgOrderBy(sel, arg)
|
||||
|
||||
case "distinct_on", "distinct":
|
||||
err, ka = com.compileArgDistinctOn(sel, arg)
|
||||
err, df = com.compileArgDistinctOn(sel, arg)
|
||||
|
||||
case "limit":
|
||||
err, ka = com.compileArgLimit(sel, arg)
|
||||
err, df = com.compileArgLimit(sel, arg)
|
||||
|
||||
case "offset":
|
||||
err, ka = com.compileArgOffset(sel, arg)
|
||||
err, df = com.compileArgOffset(sel, arg)
|
||||
|
||||
case "first":
|
||||
err, df = com.compileArgFirstLast(sel, arg, PtForward)
|
||||
|
||||
case "last":
|
||||
err, df = com.compileArgFirstLast(sel, arg, PtBackward)
|
||||
|
||||
case "after":
|
||||
err, df = com.compileArgAfterBefore(sel, arg, PtForward)
|
||||
|
||||
case "before":
|
||||
err, df = com.compileArgAfterBefore(sel, arg, PtBackward)
|
||||
}
|
||||
|
||||
if !ka {
|
||||
nodePool.Put(arg.Val)
|
||||
if !df {
|
||||
FreeNode(arg.Val, 5)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -480,7 +500,7 @@ func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg, role string
|
||||
func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
|
||||
setActionVar := func(arg *Arg) error {
|
||||
if arg.Val.Type != NodeVar {
|
||||
return fmt.Errorf("value for argument '%s' must be a variable", arg.Name)
|
||||
return argErr(arg.Name, "variable")
|
||||
}
|
||||
qc.ActionVar = arg.Val.Val
|
||||
return nil
|
||||
@ -503,7 +523,7 @@ func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
|
||||
qc.Type = QTDelete
|
||||
|
||||
if arg.Val.Type != NodeBool {
|
||||
return fmt.Errorf("value for argument '%s' must be a boolean", arg.Name)
|
||||
return argErr(arg.Name, "boolen")
|
||||
}
|
||||
|
||||
if arg.Val.Val == "false" {
|
||||
@ -529,7 +549,7 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
||||
var needsUser bool
|
||||
|
||||
if node == nil || len(node.Children) == 0 {
|
||||
return nil, needsUser, errors.New("invalid argument value")
|
||||
return nil, false, errors.New("invalid argument value")
|
||||
}
|
||||
|
||||
pushChild(st, nil, node)
|
||||
@ -540,6 +560,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, needsUser, fmt.Errorf("16: unexpected value %v (%t)", intf, intf)
|
||||
@ -576,19 +597,23 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*
|
||||
}
|
||||
}
|
||||
|
||||
pushChild(st, nil, node)
|
||||
if usePool {
|
||||
st.Push(node)
|
||||
|
||||
for {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
}
|
||||
intf := st.Pop()
|
||||
node, _ := intf.(*Node)
|
||||
|
||||
node, ok := intf.(*Node)
|
||||
if !ok || node == nil {
|
||||
continue
|
||||
}
|
||||
for i := range node.Children {
|
||||
st.Push(node.Children[i])
|
||||
}
|
||||
nodePool.Put(node)
|
||||
FreeNode(node, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return root, needsUser, nil
|
||||
@ -603,60 +628,40 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if arg.Val.Type != NodeVar {
|
||||
return argErr("id", "variable"), false
|
||||
}
|
||||
|
||||
ex := expPool.Get().(*Exp)
|
||||
ex.Reset()
|
||||
|
||||
ex.Op = OpEqID
|
||||
ex.Val = arg.Val.Val
|
||||
|
||||
switch arg.Val.Type {
|
||||
case NodeStr:
|
||||
ex.Type = ValStr
|
||||
case NodeInt:
|
||||
ex.Type = ValInt
|
||||
case NodeFloat:
|
||||
ex.Type = ValFloat
|
||||
case NodeVar:
|
||||
ex.Type = ValVar
|
||||
default:
|
||||
return fmt.Errorf("expecting a string, int, float or variable"), false
|
||||
}
|
||||
ex.Val = arg.Val.Val
|
||||
|
||||
sel.Where = ex
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) {
|
||||
if arg.Val.Type != NodeVar {
|
||||
return argErr("search", "variable"), false
|
||||
}
|
||||
|
||||
ex := expPool.Get().(*Exp)
|
||||
ex.Reset()
|
||||
|
||||
ex.Op = OpTsQuery
|
||||
ex.Val = arg.Val.Val
|
||||
|
||||
if arg.Val.Type == NodeVar {
|
||||
ex.Type = ValVar
|
||||
} else {
|
||||
ex.Type = ValStr
|
||||
}
|
||||
ex.Val = arg.Val.Val
|
||||
|
||||
if sel.Args == nil {
|
||||
sel.Args = make(map[string]*Node)
|
||||
}
|
||||
|
||||
sel.Args[arg.Name] = arg.Val
|
||||
AddFilter(sel, ex)
|
||||
|
||||
if sel.Where != nil {
|
||||
ow := sel.Where
|
||||
|
||||
sel.Where = expPool.Get().(*Exp)
|
||||
sel.Where.Reset()
|
||||
sel.Where.Op = OpAnd
|
||||
sel.Where.Children = sel.Where.childrenA[:2]
|
||||
sel.Where.Children[0] = ex
|
||||
sel.Where.Children[1] = ow
|
||||
} else {
|
||||
sel.Where = ex
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
|
||||
@ -672,21 +677,9 @@ func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) (error,
|
||||
if nu && role == "anon" {
|
||||
sel.SkipRender = true
|
||||
}
|
||||
AddFilter(sel, ex)
|
||||
|
||||
if sel.Where != nil {
|
||||
ow := sel.Where
|
||||
|
||||
sel.Where = expPool.Get().(*Exp)
|
||||
sel.Where.Reset()
|
||||
sel.Where.Op = OpAnd
|
||||
sel.Where.Children = sel.Where.childrenA[:2]
|
||||
sel.Where.Children[0] = ex
|
||||
sel.Where.Children[1] = ow
|
||||
} else {
|
||||
sel.Where = ex
|
||||
}
|
||||
|
||||
return nil, false
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
||||
@ -713,16 +706,12 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
||||
}
|
||||
|
||||
if _, ok := com.bl[node.Name]; ok {
|
||||
nodePool.Put(node)
|
||||
FreeNode(node, 2)
|
||||
continue
|
||||
}
|
||||
|
||||
if node.Type == NodeObj {
|
||||
for i := range node.Children {
|
||||
st.Push(node.Children[i])
|
||||
}
|
||||
nodePool.Put(node)
|
||||
continue
|
||||
if node.Type != NodeStr && node.Type != NodeVar {
|
||||
return fmt.Errorf("expecting a string or variable"), false
|
||||
}
|
||||
|
||||
ob := &OrderBy{}
|
||||
@ -746,7 +735,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
||||
|
||||
setOrderByColName(ob, node)
|
||||
sel.OrderBy = append(sel.OrderBy, ob)
|
||||
nodePool.Put(node)
|
||||
FreeNode(node, 3)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@ -768,7 +757,7 @@ func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) (error, bool) {
|
||||
|
||||
for i := range node.Children {
|
||||
sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val)
|
||||
nodePool.Put(node.Children[i])
|
||||
FreeNode(node.Children[i], 5)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
@ -778,7 +767,7 @@ func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if node.Type != NodeInt {
|
||||
return fmt.Errorf("expecting an integer"), false
|
||||
return argErr("limit", "number"), false
|
||||
}
|
||||
|
||||
sel.Paging.Limit = node.Val
|
||||
@ -789,14 +778,39 @@ func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) (error, bool) {
|
||||
func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if node.Type != NodeInt {
|
||||
return fmt.Errorf("expecting an integer"), false
|
||||
if node.Type != NodeVar {
|
||||
return argErr("offset", "variable"), false
|
||||
}
|
||||
|
||||
sel.Paging.Offset = node.Val
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgFirstLast(sel *Select, arg *Arg, pt PagingType) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if node.Type != NodeInt {
|
||||
return argErr(arg.Name, "number"), false
|
||||
}
|
||||
|
||||
sel.Paging.Type = pt
|
||||
sel.Paging.Limit = node.Val
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgAfterBefore(sel *Select, arg *Arg, pt PagingType) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if node.Type != NodeVar || node.Val != "cursor" {
|
||||
return fmt.Errorf("value for argument '%s' must be a variable named $cursor", arg.Name), false
|
||||
}
|
||||
sel.Paging.Type = pt
|
||||
sel.Paging.Cursor = true
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var zeroTrv = &trval{}
|
||||
|
||||
func (com *Compiler) getRole(role, field string) *trval {
|
||||
@ -807,6 +821,27 @@ func (com *Compiler) getRole(role, field string) *trval {
|
||||
}
|
||||
}
|
||||
|
||||
func AddFilter(sel *Select, fil *Exp) {
|
||||
if sel.Where != nil {
|
||||
ow := sel.Where
|
||||
|
||||
if sel.Where.Op != OpAnd || !sel.Where.doFree {
|
||||
sel.Where = expPool.Get().(*Exp)
|
||||
sel.Where.Reset()
|
||||
sel.Where.Op = OpAnd
|
||||
sel.Where.Children = sel.Where.childrenA[:2]
|
||||
sel.Where.Children[0] = fil
|
||||
sel.Where.Children[1] = ow
|
||||
|
||||
} else {
|
||||
sel.Where.Children = append(sel.Where.Children, fil)
|
||||
}
|
||||
|
||||
} else {
|
||||
sel.Where = fil
|
||||
}
|
||||
}
|
||||
|
||||
func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
||||
name := node.Name
|
||||
if name[0] == '_' {
|
||||
@ -821,6 +856,7 @@ func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
||||
} else {
|
||||
ex = &Exp{doFree: false}
|
||||
}
|
||||
|
||||
ex.Children = ex.childrenA[:0]
|
||||
|
||||
switch name {
|
||||
@ -902,6 +938,12 @@ func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
||||
case "is_null":
|
||||
ex.Op = OpIsNull
|
||||
ex.Val = node.Val
|
||||
case "null_eq", "ndis", "not_distinct":
|
||||
ex.Op = OpNotDistinct
|
||||
ex.Val = node.Val
|
||||
case "null_neq", "dis", "distinct":
|
||||
ex.Op = OpDistinct
|
||||
ex.Val = node.Val
|
||||
default:
|
||||
pushChildren(st, node.exp, node)
|
||||
return nil, nil // skip node
|
||||
@ -997,7 +1039,6 @@ func pushChildren(st *util.Stack, exp *Exp, node *Node) {
|
||||
func pushChild(st *util.Stack, exp *Exp, node *Node) {
|
||||
node.Children[0].exp = exp
|
||||
st.Push(node.Children[0])
|
||||
|
||||
}
|
||||
|
||||
func compileFilter(filter []string) (*Exp, bool, error) {
|
||||
@ -1130,3 +1171,7 @@ func FreeExp(ex *Exp) {
|
||||
expPool.Put(ex)
|
||||
}
|
||||
}
|
||||
|
||||
func argErr(name, ty string) error {
|
||||
return fmt.Errorf("value for argument '%s' must be a %s", name, ty)
|
||||
}
|
||||
|
@ -4,8 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/adjust/gorails/marshal"
|
||||
)
|
||||
|
||||
@ -37,17 +38,20 @@ func NewAuth(version, secret string) (*Auth, error) {
|
||||
AuthSalt: authSalt,
|
||||
}
|
||||
|
||||
ver, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rails auth: %s", err)
|
||||
var v1, v2 int
|
||||
var err error
|
||||
|
||||
sv := strings.Split(version, ".")
|
||||
if len(sv) >= 2 {
|
||||
if v1, err = strconv.Atoi(sv[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v2, err = strconv.Atoi(sv[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
gt52, err := semver.NewConstraint(">= 5.2")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rails auth: %s", err)
|
||||
}
|
||||
|
||||
if gt52.Check(ver) {
|
||||
if v1 >= 5 && v2 >= 2 {
|
||||
ra.Cipher = railsCipher52
|
||||
} else {
|
||||
ra.Cipher = railsCipher
|
||||
|
7
scripts/start.sh
Executable file
7
scripts/start.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
if test -f "./config/$SECRETS_FILE"
|
||||
then
|
||||
./sops --config ./config exec-env "./config/$SECRETS_FILE" "$*"
|
||||
else
|
||||
$@
|
||||
fi
|
41
serv/actions.go
Normal file
41
serv/actions.go
Normal file
@ -0,0 +1,41 @@
|
||||
package serv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type actionFn func(w http.ResponseWriter, r *http.Request) error
|
||||
|
||||
func newAction(a configAction) (http.Handler, error) {
|
||||
var fn actionFn
|
||||
var err error
|
||||
|
||||
if len(a.SQL) != 0 {
|
||||
fn, err = newSQLAction(a)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid config for action '%s'", a.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpFn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := fn(w, r); err != nil {
|
||||
errlog.Error().Err(err).Send()
|
||||
errorResp(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
return http.HandlerFunc(httpFn), nil
|
||||
}
|
||||
|
||||
func newSQLAction(a configAction) (actionFn, error) {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) error {
|
||||
_, err := db.Exec(r.Context(), a.SQL)
|
||||
return err
|
||||
}
|
||||
|
||||
return fn, nil
|
||||
}
|
49
serv/args.go
49
serv/args.go
@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@ -18,31 +17,46 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
|
||||
if v := ctx.Value(userIDProviderKey); v != nil {
|
||||
return io.WriteString(w, v.(string))
|
||||
}
|
||||
return 0, errors.New("query requires variable $user_id_provider")
|
||||
return 0, argErr("user_id_provider")
|
||||
|
||||
case "user_id":
|
||||
if v := ctx.Value(userIDKey); v != nil {
|
||||
return io.WriteString(w, v.(string))
|
||||
}
|
||||
return 0, errors.New("query requires variable $user_id")
|
||||
return 0, argErr("user_id")
|
||||
|
||||
case "user_role":
|
||||
if v := ctx.Value(userRoleKey); v != nil {
|
||||
return io.WriteString(w, v.(string))
|
||||
}
|
||||
return 0, errors.New("query requires variable $user_role")
|
||||
return 0, argErr("user_role")
|
||||
}
|
||||
|
||||
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
|
||||
|
||||
if len(fields) == 0 {
|
||||
return 0, nil
|
||||
return 0, argErr(tag)
|
||||
|
||||
}
|
||||
v := fields[0].Value
|
||||
|
||||
// Open and close quotes
|
||||
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
||||
fields[0].Value = v[1 : len(v)-1]
|
||||
}
|
||||
|
||||
if tag == "cursor" {
|
||||
if bytes.EqualFold(v, []byte("null")) {
|
||||
return io.WriteString(w, ``)
|
||||
}
|
||||
v1, err := decrypt(string(fields[0].Value))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return w.Write(v1)
|
||||
}
|
||||
|
||||
return w.Write(escQuote(fields[0].Value))
|
||||
}
|
||||
}
|
||||
@ -63,27 +77,37 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
||||
|
||||
for i := range args {
|
||||
av := args[i]
|
||||
|
||||
switch {
|
||||
case bytes.Equal(av, []byte("user_id")):
|
||||
if v := ctx.Value(userIDKey); v != nil {
|
||||
vars[i] = v.(string)
|
||||
} else {
|
||||
return nil, errors.New("query requires variable $user_id")
|
||||
return nil, argErr("user_id")
|
||||
}
|
||||
|
||||
case bytes.Equal(av, []byte("user_id_provider")):
|
||||
if v := ctx.Value(userIDProviderKey); v != nil {
|
||||
vars[i] = v.(string)
|
||||
} else {
|
||||
return nil, errors.New("query requires variable $user_id_provider")
|
||||
return nil, argErr("user_id_provider")
|
||||
}
|
||||
|
||||
case bytes.Equal(av, []byte("user_role")):
|
||||
if v := ctx.Value(userRoleKey); v != nil {
|
||||
vars[i] = v.(string)
|
||||
} else {
|
||||
return nil, errors.New("query requires variable $user_role")
|
||||
return nil, argErr("user_role")
|
||||
}
|
||||
|
||||
case bytes.Equal(av, []byte("cursor")):
|
||||
if v, ok := fields["cursor"]; ok && v[0] == '"' {
|
||||
v1, err := decrypt(string(v[1 : len(v)-1]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vars[i] = v1
|
||||
} else {
|
||||
return nil, argErr("cursor")
|
||||
}
|
||||
|
||||
default:
|
||||
@ -96,11 +120,12 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
||||
if err := json.Unmarshal(v, &val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vars[i] = val
|
||||
}
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("query requires variable $%s", string(av))
|
||||
return nil, argErr(string(av))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,3 +160,7 @@ func escQuote(b []byte) []byte {
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func argErr(name string) error {
|
||||
return fmt.Errorf("query requires variable '%s' to be set", name)
|
||||
}
|
||||
|
60
serv/auth.go
60
serv/auth.go
@ -3,7 +3,6 @@ package serv
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ctxkey int
|
||||
@ -14,7 +13,7 @@ const (
|
||||
userRoleKey
|
||||
)
|
||||
|
||||
func headerAuth(next http.Handler) http.HandlerFunc {
|
||||
func headerAuth(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@ -37,28 +36,53 @@ func headerAuth(next http.Handler) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func withAuth(next http.Handler) http.Handler {
|
||||
at := conf.Auth.Type
|
||||
ru := conf.Auth.Rails.URL
|
||||
func headerHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
hdr := authc.Header
|
||||
|
||||
if conf.Auth.CredsInHeader {
|
||||
next = headerAuth(next)
|
||||
if len(hdr.Name) == 0 {
|
||||
errlog.Fatal().Str("auth", authc.Name).Msg("no header.name defined")
|
||||
}
|
||||
|
||||
switch at {
|
||||
if !hdr.Exists && len(hdr.Value) == 0 {
|
||||
errlog.Fatal().Str("auth", authc.Name).Msg("no header.value defined")
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var fo1 bool
|
||||
value := r.Header.Get(hdr.Name)
|
||||
|
||||
switch {
|
||||
case hdr.Exists:
|
||||
fo1 = (len(value) == 0)
|
||||
|
||||
default:
|
||||
fo1 = (value != hdr.Value)
|
||||
}
|
||||
|
||||
if fo1 {
|
||||
http.Error(w, "401 unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func withAuth(next http.Handler, authc configAuth) http.Handler {
|
||||
if authc.CredsInHeader {
|
||||
next = headerAuth(authc, next)
|
||||
}
|
||||
|
||||
switch authc.Type {
|
||||
case "rails":
|
||||
if strings.HasPrefix(ru, "memcache:") {
|
||||
return railsMemcacheHandler(next)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ru, "redis:") {
|
||||
return railsRedisHandler(next)
|
||||
}
|
||||
|
||||
return railsCookieHandler(next)
|
||||
return railsHandler(authc, next)
|
||||
|
||||
case "jwt":
|
||||
return jwtHandler(next)
|
||||
return jwtHandler(authc, next)
|
||||
|
||||
case "header":
|
||||
return headerHandler(authc, next)
|
||||
|
||||
}
|
||||
|
||||
return next
|
||||
|
@ -14,18 +14,18 @@ const (
|
||||
jwtAuth0 int = iota + 1
|
||||
)
|
||||
|
||||
func jwtHandler(next http.Handler) http.HandlerFunc {
|
||||
func jwtHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
var key interface{}
|
||||
var jwtProvider int
|
||||
|
||||
cookie := conf.Auth.Cookie
|
||||
cookie := authc.Cookie
|
||||
|
||||
if conf.Auth.JWT.Provider == "auth0" {
|
||||
if authc.JWT.Provider == "auth0" {
|
||||
jwtProvider = jwtAuth0
|
||||
}
|
||||
|
||||
secret := conf.Auth.JWT.Secret
|
||||
publicKeyFile := conf.Auth.JWT.PubKeyFile
|
||||
secret := authc.JWT.Secret
|
||||
publicKeyFile := authc.JWT.PubKeyFile
|
||||
|
||||
switch {
|
||||
case len(secret) != 0:
|
||||
@ -37,7 +37,7 @@ func jwtHandler(next http.Handler) http.HandlerFunc {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
switch conf.Auth.JWT.PubKeyType {
|
||||
switch authc.JWT.PubKeyType {
|
||||
case "ecdsa":
|
||||
key, err = jwt.ParseECPublicKeyFromPEM(kd)
|
||||
|
||||
|
@ -6,32 +6,47 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
"github.com/dosco/super-graph/rails"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
)
|
||||
|
||||
func railsRedisHandler(next http.Handler) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
func railsHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
ru := authc.Rails.URL
|
||||
|
||||
if strings.HasPrefix(ru, "memcache:") {
|
||||
return railsMemcacheHandler(authc, next)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ru, "redis:") {
|
||||
return railsRedisHandler(authc, next)
|
||||
}
|
||||
|
||||
return railsCookieHandler(authc, next)
|
||||
}
|
||||
|
||||
func railsRedisHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
cookie := authc.Cookie
|
||||
if len(cookie) == 0 {
|
||||
errlog.Fatal().Msg("no auth.cookie defined")
|
||||
}
|
||||
|
||||
if len(conf.Auth.Rails.URL) == 0 {
|
||||
if len(authc.Rails.URL) == 0 {
|
||||
errlog.Fatal().Msg("no auth.rails.url defined")
|
||||
}
|
||||
|
||||
rp := &redis.Pool{
|
||||
MaxIdle: conf.Auth.Rails.MaxIdle,
|
||||
MaxActive: conf.Auth.Rails.MaxActive,
|
||||
MaxIdle: authc.Rails.MaxIdle,
|
||||
MaxActive: authc.Rails.MaxActive,
|
||||
Dial: func() (redis.Conn, error) {
|
||||
c, err := redis.DialURL(conf.Auth.Rails.URL)
|
||||
c, err := redis.DialURL(authc.Rails.URL)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
pwd := conf.Auth.Rails.Password
|
||||
pwd := authc.Rails.Password
|
||||
if len(pwd) != 0 {
|
||||
if _, err := c.Do("AUTH", pwd); err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
@ -66,17 +81,17 @@ func railsRedisHandler(next http.Handler) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func railsMemcacheHandler(next http.Handler) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
func railsMemcacheHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
cookie := authc.Cookie
|
||||
if len(cookie) == 0 {
|
||||
errlog.Fatal().Msg("no auth.cookie defined")
|
||||
}
|
||||
|
||||
if len(conf.Auth.Rails.URL) == 0 {
|
||||
if len(authc.Rails.URL) == 0 {
|
||||
errlog.Fatal().Msg("no auth.rails.url defined")
|
||||
}
|
||||
|
||||
rURL, err := url.Parse(conf.Auth.Rails.URL)
|
||||
rURL, err := url.Parse(authc.Rails.URL)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
@ -108,13 +123,13 @@ func railsMemcacheHandler(next http.Handler) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func railsCookieHandler(next http.Handler) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
func railsCookieHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
cookie := authc.Cookie
|
||||
if len(cookie) == 0 {
|
||||
errlog.Fatal().Msg("no auth.cookie defined")
|
||||
}
|
||||
|
||||
ra, err := railsAuth(conf)
|
||||
ra, err := railsAuth(authc)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
@ -139,13 +154,13 @@ func railsCookieHandler(next http.Handler) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func railsAuth(c *config) (*rails.Auth, error) {
|
||||
secret := c.Auth.Rails.SecretKeyBase
|
||||
func railsAuth(authc configAuth) (*rails.Auth, error) {
|
||||
secret := authc.Rails.SecretKeyBase
|
||||
if len(secret) == 0 {
|
||||
return nil, errors.New("no auth.rails.secret_key_base defined")
|
||||
}
|
||||
|
||||
version := c.Auth.Rails.Version
|
||||
version := authc.Rails.Version
|
||||
if len(version) == 0 {
|
||||
return nil, errors.New("no auth.rails.version defined")
|
||||
}
|
||||
@ -155,16 +170,16 @@ func railsAuth(c *config) (*rails.Auth, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.Auth.Rails.Salt) != 0 {
|
||||
ra.Salt = c.Auth.Rails.Salt
|
||||
if len(authc.Rails.Salt) != 0 {
|
||||
ra.Salt = authc.Rails.Salt
|
||||
}
|
||||
|
||||
if len(conf.Auth.Rails.SignSalt) != 0 {
|
||||
ra.SignSalt = c.Auth.Rails.SignSalt
|
||||
if len(authc.Rails.SignSalt) != 0 {
|
||||
ra.SignSalt = authc.Rails.SignSalt
|
||||
}
|
||||
|
||||
if len(conf.Auth.Rails.AuthSalt) != 0 {
|
||||
ra.AuthSalt = c.Auth.Rails.AuthSalt
|
||||
if len(authc.Rails.AuthSalt) != 0 {
|
||||
ra.AuthSalt = authc.Rails.AuthSalt
|
||||
}
|
||||
|
||||
return ra, nil
|
||||
|
@ -38,6 +38,8 @@ var (
|
||||
allowList *allow.List // allow.list is contains queries allowed in production
|
||||
qcompile *qcode.Compiler // qcode compiler
|
||||
pcompile *psql.Compiler // postgres sql compiler
|
||||
secretKey [32]byte // encryption key
|
||||
internalKey [32]byte // encryption key used for internal needs
|
||||
)
|
||||
|
||||
func Cmd() {
|
||||
|
@ -311,3 +311,13 @@ func getMigrationVars() map[string]interface{} {
|
||||
"env": strings.ToLower(os.Getenv("GO_ENV")),
|
||||
}
|
||||
}
|
||||
|
||||
func initConfOnce() {
|
||||
var err error
|
||||
|
||||
if conf == nil {
|
||||
if conf, err = initConf(); err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to read config")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
120
serv/cmd_seed.go
120
serv/cmd_seed.go
@ -3,15 +3,20 @@ package serv
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/brianvoe/gofakeit"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/valyala/fasttemplate"
|
||||
)
|
||||
@ -41,6 +46,7 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
|
||||
|
||||
vm := goja.New()
|
||||
vm.Set("graphql", graphQLFunc)
|
||||
vm.Set("import_csv", importCSV)
|
||||
|
||||
console := vm.NewObject()
|
||||
console.Set("log", logFunc) //nolint: errcheck
|
||||
@ -128,6 +134,106 @@ func graphQLFunc(query string, data interface{}, opt map[string]string) map[stri
|
||||
return val
|
||||
}
|
||||
|
||||
type csvSource struct {
|
||||
rows [][]string
|
||||
i int
|
||||
}
|
||||
|
||||
func NewCSVSource(filename string) (*csvSource, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
r := csv.NewReader(f)
|
||||
rows, err := r.ReadAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &csvSource{rows: rows}, nil
|
||||
}
|
||||
|
||||
func (c *csvSource) Next() bool {
|
||||
return c.i < len(c.rows)
|
||||
}
|
||||
|
||||
func (c *csvSource) Values() ([]interface{}, error) {
|
||||
var vals []interface{}
|
||||
var err error
|
||||
|
||||
for _, v := range c.rows[c.i] {
|
||||
switch {
|
||||
case len(v) == 0:
|
||||
vals = append(vals, "")
|
||||
case isDigit(v):
|
||||
var n int
|
||||
if n, err = strconv.Atoi(v); err == nil {
|
||||
vals = append(vals, n)
|
||||
}
|
||||
case strings.EqualFold(v, "true") || strings.EqualFold(v, "false"):
|
||||
var b bool
|
||||
if b, err = strconv.ParseBool(v); err == nil {
|
||||
vals = append(vals, b)
|
||||
}
|
||||
default:
|
||||
vals = append(vals, v)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w (line no %d)", err, c.i)
|
||||
}
|
||||
}
|
||||
c.i++
|
||||
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func isDigit(v string) bool {
|
||||
for i := range v {
|
||||
if v[i] < '0' || v[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *csvSource) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func importCSV(table, filename string) int64 {
|
||||
if filename[0] != '/' {
|
||||
filename = path.Join(confPath, filename)
|
||||
}
|
||||
|
||||
s, err := NewCSVSource(filename)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
var cols []string
|
||||
colval, _ := s.Values()
|
||||
|
||||
for _, c := range colval {
|
||||
cols = append(cols, c.(string))
|
||||
}
|
||||
|
||||
n, err := db.CopyFrom(
|
||||
context.Background(),
|
||||
pgx.Identifier{table},
|
||||
cols,
|
||||
s)
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w (line no %d)", err, s.i)
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
//nolint: errcheck
|
||||
func logFunc(args ...interface{}) {
|
||||
for _, arg := range args {
|
||||
@ -145,6 +251,17 @@ func logFunc(args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func avatarURL(size int) string {
|
||||
if size == 0 {
|
||||
size = 200
|
||||
}
|
||||
return fmt.Sprintf("https://i.pravatar.cc/%d?%d", size, rand.Intn(5000))
|
||||
}
|
||||
|
||||
func imageURL(width int, height int) string {
|
||||
return fmt.Sprintf("https://picsum.photos/%d/%d?%d", width, height, rand.Intn(5000))
|
||||
}
|
||||
|
||||
//nolint: errcheck
|
||||
func setFakeFuncs(f *goja.Object) {
|
||||
gofakeit.Seed(0)
|
||||
@ -222,7 +339,8 @@ func setFakeFuncs(f *goja.Object) {
|
||||
|
||||
// Internet
|
||||
f.Set("url", gofakeit.URL)
|
||||
f.Set("image_url", gofakeit.ImageURL)
|
||||
f.Set("image_url", imageURL)
|
||||
f.Set("avatar_url", avatarURL)
|
||||
f.Set("domain_name", gofakeit.DomainName)
|
||||
f.Set("domain_suffix", gofakeit.DomainSuffix)
|
||||
f.Set("ipv4_address", gofakeit.IPv4Address)
|
||||
|
@ -7,23 +7,25 @@ import (
|
||||
func cmdServ(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
initWatcher(confPath)
|
||||
|
||||
if conf, err = initConf(); err != nil {
|
||||
fatalInProd(err, "failed to read config")
|
||||
}
|
||||
|
||||
if conf != nil {
|
||||
db, err = initDBPool(conf)
|
||||
|
||||
if err == nil {
|
||||
initCompiler()
|
||||
initAllowList(confPath)
|
||||
initPreparedList(confPath)
|
||||
} else {
|
||||
if err != nil {
|
||||
fatalInProd(err, "failed to connect to database")
|
||||
}
|
||||
}
|
||||
|
||||
initWatcher(confPath)
|
||||
if conf != nil && db != nil {
|
||||
initCrypto()
|
||||
initCompiler()
|
||||
initResolvers()
|
||||
initAllowList(confPath)
|
||||
initPreparedList(confPath)
|
||||
}
|
||||
|
||||
startHTTP()
|
||||
}
|
||||
|
135
serv/config.go
135
serv/config.go
@ -30,10 +30,44 @@ type config struct {
|
||||
AuthFailBlock bool `mapstructure:"auth_fail_block"`
|
||||
SeedFile string `mapstructure:"seed_file"`
|
||||
MigrationsPath string `mapstructure:"migrations_path"`
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
|
||||
Inflections map[string]string
|
||||
|
||||
Auth struct {
|
||||
Auth configAuth
|
||||
Auths []configAuth
|
||||
|
||||
DB struct {
|
||||
Type string
|
||||
Host string
|
||||
Port uint16
|
||||
DBName string
|
||||
User string
|
||||
Password string
|
||||
Schema string
|
||||
PoolSize int32 `mapstructure:"pool_size"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
SetUserID bool `mapstructure:"set_user_id"`
|
||||
PingTimeout time.Duration `mapstructure:"ping_timeout"`
|
||||
|
||||
Vars map[string]string `mapstructure:"variables"`
|
||||
Blocklist []string
|
||||
|
||||
Tables []configTable
|
||||
} `mapstructure:"database"`
|
||||
|
||||
Actions []configAction
|
||||
|
||||
Tables []configTable
|
||||
|
||||
RolesQuery string `mapstructure:"roles_query"`
|
||||
Roles []configRole
|
||||
roles map[string]*configRole
|
||||
abacEnabled bool
|
||||
}
|
||||
|
||||
type configAuth struct {
|
||||
Name string
|
||||
Type string
|
||||
Cookie string
|
||||
CredsInHeader bool `mapstructure:"creds_in_header"`
|
||||
@ -56,33 +90,12 @@ type config struct {
|
||||
PubKeyFile string `mapstructure:"public_key_file"`
|
||||
PubKeyType string `mapstructure:"public_key_type"`
|
||||
}
|
||||
|
||||
Header struct {
|
||||
Name string
|
||||
Value string
|
||||
Exists bool
|
||||
}
|
||||
|
||||
DB struct {
|
||||
Type string
|
||||
Host string
|
||||
Port uint16
|
||||
DBName string
|
||||
User string
|
||||
Password string
|
||||
Schema string
|
||||
PoolSize int32 `mapstructure:"pool_size"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
SetUserID bool `mapstructure:"set_user_id"`
|
||||
PingTimeout time.Duration `mapstructure:"ping_timeout"`
|
||||
|
||||
Vars map[string]string `mapstructure:"variables"`
|
||||
Blocklist []string
|
||||
|
||||
Tables []configTable
|
||||
} `mapstructure:"database"`
|
||||
|
||||
Tables []configTable
|
||||
|
||||
RolesQuery string `mapstructure:"roles_query"`
|
||||
Roles []configRole
|
||||
roles map[string]*configRole
|
||||
abacEnabled bool
|
||||
}
|
||||
|
||||
type configColumn struct {
|
||||
@ -156,6 +169,12 @@ type configRole struct {
|
||||
tablesMap map[string]*configRoleTable
|
||||
}
|
||||
|
||||
type configAction struct {
|
||||
Name string
|
||||
SQL string
|
||||
AuthName string `mapstructure:"auth_name"`
|
||||
}
|
||||
|
||||
func newConfig(name string) *viper.Viper {
|
||||
vi := viper.New()
|
||||
|
||||
@ -283,26 +302,48 @@ func (c *config) Init(vi *viper.Viper) error {
|
||||
func (c *config) validate() {
|
||||
rm := make(map[string]struct{})
|
||||
|
||||
for i := range c.Roles {
|
||||
name := c.Roles[i].Name
|
||||
for _, v := range c.Roles {
|
||||
name := strings.ToLower(v.Name)
|
||||
|
||||
if _, ok := rm[name]; ok {
|
||||
errlog.Fatal().Msgf("duplicate config for role '%s'", c.Roles[i].Name)
|
||||
errlog.Fatal().Msgf("duplicate config for role '%s'", v.Name)
|
||||
}
|
||||
rm[name] = struct{}{}
|
||||
}
|
||||
|
||||
tm := make(map[string]struct{})
|
||||
|
||||
for i := range c.Tables {
|
||||
name := c.Tables[i].Name
|
||||
for _, v := range c.Tables {
|
||||
name := strings.ToLower(v.Name)
|
||||
|
||||
if _, ok := tm[name]; ok {
|
||||
errlog.Fatal().Msgf("duplicate config for table '%s'", c.Tables[i].Name)
|
||||
errlog.Fatal().Msgf("duplicate config for table '%s'", v.Name)
|
||||
}
|
||||
tm[name] = struct{}{}
|
||||
}
|
||||
|
||||
am := make(map[string]struct{})
|
||||
|
||||
for _, v := range c.Auths {
|
||||
name := strings.ToLower(v.Name)
|
||||
|
||||
if _, ok := am[name]; ok {
|
||||
errlog.Fatal().Msgf("duplicate config for auth '%s'", v.Name)
|
||||
}
|
||||
am[name] = struct{}{}
|
||||
}
|
||||
|
||||
for _, v := range c.Actions {
|
||||
if len(v.AuthName) == 0 {
|
||||
continue
|
||||
}
|
||||
authName := strings.ToLower(v.AuthName)
|
||||
|
||||
if _, ok := am[authName]; !ok {
|
||||
errlog.Fatal().Msgf("invalid auth_name for action '%s'", v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.RolesQuery) == 0 {
|
||||
logger.Warn().Msgf("no 'roles_query' defined.")
|
||||
}
|
||||
@ -349,3 +390,31 @@ func sanitize(s string) string {
|
||||
return strings.ToLower(m)
|
||||
})
|
||||
}
|
||||
|
||||
func getConfigName() string {
|
||||
if len(os.Getenv("GO_ENV")) == 0 {
|
||||
return "dev"
|
||||
}
|
||||
|
||||
ge := strings.ToLower(os.Getenv("GO_ENV"))
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(ge, "pro"):
|
||||
return "prod"
|
||||
|
||||
case strings.HasPrefix(ge, "sta"):
|
||||
return "stage"
|
||||
|
||||
case strings.HasPrefix(ge, "tes"):
|
||||
return "test"
|
||||
|
||||
case strings.HasPrefix(ge, "dev"):
|
||||
return "dev"
|
||||
}
|
||||
|
||||
return ge
|
||||
}
|
||||
|
||||
func isDev() bool {
|
||||
return strings.HasPrefix(os.Getenv("GO_ENV"), "dev")
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/dosco/super-graph/allow"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/valyala/fasttemplate"
|
||||
)
|
||||
@ -192,6 +193,8 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
|
||||
}
|
||||
st := &stmts[0]
|
||||
|
||||
//fmt.Println(">", string(st.sql))
|
||||
|
||||
t := fasttemplate.New(st.sql, openVar, closeVar)
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
@ -241,8 +244,12 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if root, err = encryptCursor(st.qc, root); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if allowList.IsPersist() {
|
||||
if err := allowList.Add(c.req.Vars, c.req.Query, c.req.ref); err != nil {
|
||||
if err := allowList.Set(c.req.Vars, c.req.Query, c.req.ref); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
72
serv/cursor.go
Normal file
72
serv/cursor.go
Normal file
@ -0,0 +1,72 @@
|
||||
package serv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/dosco/super-graph/crypto"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
)
|
||||
|
||||
func encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) {
|
||||
var keys [][]byte
|
||||
|
||||
for _, s := range qc.Selects {
|
||||
if s.Paging.Type != qcode.PtOffset {
|
||||
var buf bytes.Buffer
|
||||
|
||||
buf.WriteString(s.FieldName)
|
||||
buf.WriteString("_cursor")
|
||||
keys = append(keys, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
from := jsn.Get(data, keys)
|
||||
to := make([]jsn.Field, len(from))
|
||||
|
||||
for i, f := range from {
|
||||
to[i].Key = f.Key
|
||||
|
||||
if f.Value[0] != '"' || f.Value[len(f.Value)-1] != '"' {
|
||||
continue
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if len(f.Value) > 2 {
|
||||
v, err := crypto.Encrypt(f.Value[1:len(f.Value)-1], &internalKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf.WriteByte('"')
|
||||
buf.WriteString(base64.StdEncoding.EncodeToString(v))
|
||||
buf.WriteByte('"')
|
||||
} else {
|
||||
buf.WriteString(`null`)
|
||||
}
|
||||
|
||||
to[i].Value = buf.Bytes()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := jsn.Replace(&buf, data, from, to); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func decrypt(data string) ([]byte, error) {
|
||||
v, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crypto.Decrypt(v, &internalKey)
|
||||
}
|
40
serv/init.go
40
serv/init.go
@ -2,10 +2,13 @@ package serv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/dosco/super-graph/allow"
|
||||
"github.com/dosco/super-graph/crypto"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/rs/zerolog"
|
||||
@ -133,7 +136,17 @@ func initDBPool(c *config) (*pgxpool.Pool, error) {
|
||||
config.MaxConns = conf.DB.PoolSize
|
||||
}
|
||||
|
||||
db, err := pgxpool.ConnectConfig(context.Background(), config)
|
||||
var db *pgxpool.Pool
|
||||
var err error
|
||||
|
||||
for i := 1; i < 10; i++ {
|
||||
db, err = pgxpool.ConnectConfig(context.Background(), config)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(i*100) * time.Millisecond)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -148,20 +161,6 @@ func initCompiler() {
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to initialize compilers")
|
||||
}
|
||||
|
||||
if err := initResolvers(); err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to initialized resolvers")
|
||||
}
|
||||
}
|
||||
|
||||
func initConfOnce() {
|
||||
var err error
|
||||
|
||||
if conf == nil {
|
||||
if conf, err = initConf(); err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to read config")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initAllowList(cpath string) {
|
||||
@ -177,3 +176,14 @@ func initAllowList(cpath string) {
|
||||
errlog.Fatal().Err(err).Msg("failed to initialize allow list")
|
||||
}
|
||||
}
|
||||
|
||||
func initCrypto() {
|
||||
if len(conf.SecretKey) != 0 {
|
||||
secretKey = sha256.Sum256([]byte(conf.SecretKey))
|
||||
conf.SecretKey = ""
|
||||
internalKey = secretKey
|
||||
|
||||
} else {
|
||||
internalKey = crypto.NewEncryptionKey()
|
||||
}
|
||||
}
|
||||
|
@ -22,16 +22,20 @@ type resolvFn struct {
|
||||
Fn func(h http.Header, id []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
func initResolvers() error {
|
||||
func initResolvers() {
|
||||
var err error
|
||||
rmap = make(map[uint64]*resolvFn)
|
||||
|
||||
for _, t := range conf.Tables {
|
||||
err := initRemotes(t)
|
||||
err = initRemotes(t)
|
||||
if err != nil {
|
||||
return err
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to initialize resolvers")
|
||||
}
|
||||
}
|
||||
|
||||
func initRemotes(t configTable) error {
|
500
serv/rice-box.go
500
serv/rice-box.go
File diff suppressed because one or more lines are too long
92
serv/serv.go
92
serv/serv.go
@ -101,9 +101,14 @@ func startHTTP() {
|
||||
hostPort = defaultHP
|
||||
}
|
||||
|
||||
routes, err := routeHandler()
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: hostPort,
|
||||
Handler: routeHandler(),
|
||||
Handler: routes,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
@ -140,25 +145,35 @@ func startHTTP() {
|
||||
<-idleConnsClosed
|
||||
}
|
||||
|
||||
func routeHandler() http.Handler {
|
||||
var apiH http.Handler
|
||||
|
||||
if conf != nil && conf.HTTPGZip {
|
||||
gzipH := gziphandler.MustNewGzipLevelHandler(6)
|
||||
apiH = gzipH(http.HandlerFunc(apiV1))
|
||||
} else {
|
||||
apiH = http.HandlerFunc(apiV1)
|
||||
}
|
||||
|
||||
func routeHandler() (http.Handler, error) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
if conf != nil {
|
||||
mux.HandleFunc("/health", health)
|
||||
mux.Handle("/api/v1/graphql", withAuth(apiH))
|
||||
if conf == nil {
|
||||
return mux, nil
|
||||
}
|
||||
|
||||
routes := map[string]http.Handler{
|
||||
"/health": http.HandlerFunc(health),
|
||||
"/api/v1/graphql": withAuth(http.HandlerFunc(apiV1), conf.Auth),
|
||||
}
|
||||
|
||||
if err := setActionRoutes(routes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if conf.WebUI {
|
||||
mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox()))
|
||||
routes["/"] = http.FileServer(rice.MustFindBox("../web/build").HTTPBox())
|
||||
}
|
||||
|
||||
if conf.HTTPGZip {
|
||||
gz := gziphandler.MustNewGzipLevelHandler(6)
|
||||
for k, v := range routes {
|
||||
routes[k] = gz(v)
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range routes {
|
||||
mux.Handle(k, v)
|
||||
}
|
||||
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -166,33 +181,38 @@ func routeHandler() http.Handler {
|
||||
mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
return http.HandlerFunc(fn), nil
|
||||
}
|
||||
|
||||
func getConfigName() string {
|
||||
if len(os.Getenv("GO_ENV")) == 0 {
|
||||
return "dev"
|
||||
func setActionRoutes(routes map[string]http.Handler) error {
|
||||
var err error
|
||||
|
||||
for _, a := range conf.Actions {
|
||||
var fn http.Handler
|
||||
|
||||
fn, err = newAction(a)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
ge := strings.ToLower(os.Getenv("GO_ENV"))
|
||||
p := fmt.Sprintf("/api/v1/actions/%s", strings.ToLower(a.Name))
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(ge, "pro"):
|
||||
return "prod"
|
||||
|
||||
case strings.HasPrefix(ge, "sta"):
|
||||
return "stage"
|
||||
|
||||
case strings.HasPrefix(ge, "tes"):
|
||||
return "test"
|
||||
|
||||
case strings.HasPrefix(ge, "dev"):
|
||||
return "dev"
|
||||
if authc, ok := findAuth(a.AuthName); ok {
|
||||
routes[p] = withAuth(fn, authc)
|
||||
} else {
|
||||
routes[p] = fn
|
||||
}
|
||||
|
||||
return ge
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDev() bool {
|
||||
return strings.HasPrefix(os.Getenv("GO_ENV"), "dev")
|
||||
func findAuth(name string) (configAuth, bool) {
|
||||
var authc configAuth
|
||||
|
||||
for _, a := range conf.Auths {
|
||||
if strings.EqualFold(a.Name, name) {
|
||||
return a, true
|
||||
}
|
||||
}
|
||||
return authc, false
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
@ -127,9 +128,14 @@ func findStmt(role string, stmts []stmt) *stmt {
|
||||
}
|
||||
|
||||
func fatalInProd(err error, msg string) {
|
||||
if isDev() {
|
||||
errlog.Error().Err(err).Msg(msg)
|
||||
} else {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if !isDev() {
|
||||
errlog.Fatal().Err(err).Msg(msg)
|
||||
}
|
||||
|
||||
errlog.Error().Err(err).Msg(msg)
|
||||
|
||||
wg.Add(1)
|
||||
wg.Wait()
|
||||
}
|
||||
|
73
tmpl/dev.yml
73
tmpl/dev.yml
@ -32,6 +32,10 @@ reload_on_config_change: true
|
||||
# Path pointing to where the migrations can be found
|
||||
migrations_path: ./config/migrations
|
||||
|
||||
# Secret key for general encryption operations like
|
||||
# encrypting the cursor data
|
||||
secret_key: supercalifajalistics
|
||||
|
||||
# Postgres related environment Variables
|
||||
# SG_DATABASE_HOST
|
||||
# SG_DATABASE_PORT
|
||||
@ -49,7 +53,7 @@ migrations_path: ./config/migrations
|
||||
# sheep: sheep
|
||||
|
||||
auth:
|
||||
# Can be 'rails' or 'jwt'
|
||||
# Can be 'rails', 'jwt' or 'header'
|
||||
type: rails
|
||||
cookie: _{% app_name_slug %}_session
|
||||
|
||||
@ -83,13 +87,29 @@ auth:
|
||||
# public_key_file: /secrets/public_key.pem
|
||||
# public_key_type: ecdsa #rsa
|
||||
|
||||
# header:
|
||||
# name: dnt
|
||||
# exists: true
|
||||
# value: localhost:8080
|
||||
|
||||
# You can add additional named auths to use with actions
|
||||
# In this example actions using this auth can only be
|
||||
# called from the Google Appengine Cron service that
|
||||
# sets a special header to all it's requests
|
||||
auths:
|
||||
- name: from_taskqueue
|
||||
type: header
|
||||
header:
|
||||
name: X-Appengine-Cron
|
||||
exists: true
|
||||
|
||||
database:
|
||||
type: postgres
|
||||
host: db
|
||||
port: 5432
|
||||
dbname: {% app_name_slug %}_development
|
||||
user: postgres
|
||||
password: ''
|
||||
password: postgres
|
||||
|
||||
#schema: "public"
|
||||
#pool_size: 10
|
||||
@ -105,7 +125,9 @@ database:
|
||||
|
||||
# Define additional variables here to be used with filters
|
||||
variables:
|
||||
admin_account_id: "5"
|
||||
#admin_account_id: "5"
|
||||
admin_account_id: "sql:select id from users where admin = true limit 1"
|
||||
|
||||
|
||||
# Field and table names that you wish to block
|
||||
blocklist:
|
||||
@ -116,6 +138,16 @@ database:
|
||||
- encrypted
|
||||
- token
|
||||
|
||||
# Create custom actions with their own api endpoints
|
||||
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
|
||||
# A request to this url will execute the configured SQL query
|
||||
# which in this case refreshes a materialized view in the database.
|
||||
# The auth_name is from one of the configured auths
|
||||
actions:
|
||||
- name: refresh_leaderboard_users
|
||||
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||
auth_name: from_taskqueue
|
||||
|
||||
tables:
|
||||
- name: customers
|
||||
remotes:
|
||||
@ -137,26 +169,15 @@ tables:
|
||||
name: me
|
||||
table: users
|
||||
|
||||
roles_query: "SELECT * FROM users WHERE id = $user_id"
|
||||
|
||||
#roles_query: "SELECT * FROM users WHERE id = $user_id"
|
||||
|
||||
roles:
|
||||
- name: anon
|
||||
tables:
|
||||
- name: products
|
||||
limit: 10
|
||||
|
||||
- name: users
|
||||
query:
|
||||
columns: ["id", "name", "description" ]
|
||||
aggregation: false
|
||||
|
||||
insert:
|
||||
block: false
|
||||
|
||||
update:
|
||||
block: false
|
||||
|
||||
delete:
|
||||
block: false
|
||||
limit: 10
|
||||
|
||||
- name: user
|
||||
tables:
|
||||
@ -168,28 +189,24 @@ roles:
|
||||
query:
|
||||
limit: 50
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns: ["id", "name", "description" ]
|
||||
disable_functions: false
|
||||
|
||||
insert:
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns: ["id", "name", "description" ]
|
||||
presets:
|
||||
- user_id: "$user_id"
|
||||
- created_at: "now"
|
||||
|
||||
update:
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns:
|
||||
- id
|
||||
- name
|
||||
presets:
|
||||
- updated_at: "now"
|
||||
|
||||
delete:
|
||||
block: true
|
||||
|
||||
- name: admin
|
||||
match: id = 1000
|
||||
tables:
|
||||
- name: users
|
||||
filters: []
|
||||
# - name: admin
|
||||
# match: id = 1000
|
||||
# tables:
|
||||
# - name: users
|
||||
# filters: []
|
||||
|
@ -1,14 +1,56 @@
|
||||
version: '3.4'
|
||||
services:
|
||||
# Postgres DB
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:12
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
# Yugabyte DB
|
||||
# yb-master:
|
||||
# image: yugabytedb/yugabyte:latest
|
||||
# container_name: yb-master-n1
|
||||
# command: [ "/home/yugabyte/bin/yb-master",
|
||||
# "--fs_data_dirs=/mnt/disk0,/mnt/disk1",
|
||||
# "--master_addresses=yb-master-n1:7100",
|
||||
# "--replication_factor=1",
|
||||
# "--enable_ysql=true"]
|
||||
# ports:
|
||||
# - "7000:7000"
|
||||
# environment:
|
||||
# SERVICE_7000_NAME: yb-master
|
||||
|
||||
# db:
|
||||
# image: yugabytedb/yugabyte:latest
|
||||
# container_name: yb-tserver-n1
|
||||
# command: [ "/home/yugabyte/bin/yb-tserver",
|
||||
# "--fs_data_dirs=/mnt/disk0,/mnt/disk1",
|
||||
# "--start_pgsql_proxy",
|
||||
# "--tserver_master_addrs=yb-master-n1:7100"]
|
||||
# ports:
|
||||
# - "9042:9042"
|
||||
# - "6379:6379"
|
||||
# - "5433:5433"
|
||||
# - "9000:9000"
|
||||
# environment:
|
||||
# SERVICE_5433_NAME: ysql
|
||||
# SERVICE_9042_NAME: ycql
|
||||
# SERVICE_6379_NAME: yedis
|
||||
# SERVICE_9000_NAME: yb-tserver
|
||||
# depends_on:
|
||||
# - yb-master
|
||||
|
||||
{% app_name_slug %}_api:
|
||||
image: dosco/super-graph:latest
|
||||
environment:
|
||||
GO_ENV: "development"
|
||||
# Uncomment below for Yugabyte DB
|
||||
# SG_DATABASE_PORT: 5433
|
||||
# SG_DATABASE_USER: yugabyte
|
||||
# SG_DATABASE_PASSWORD: yugabyte
|
||||
volumes:
|
||||
- ./config:/config
|
||||
ports:
|
||||
|
@ -32,6 +32,10 @@ enable_tracing: true
|
||||
# Path pointing to where the migrations can be found
|
||||
# migrations_path: migrations
|
||||
|
||||
# Secret key for general encryption operations like
|
||||
# encrypting the cursor data
|
||||
# secret_key: supercalifajalistics
|
||||
|
||||
# Postgres related environment Variables
|
||||
# SG_DATABASE_HOST
|
||||
# SG_DATABASE_PORT
|
||||
@ -48,9 +52,9 @@ database:
|
||||
type: postgres
|
||||
host: db
|
||||
port: 5432
|
||||
dbname: {% app_name_slug %}_development
|
||||
dbname: {% app_name_slug %}_production
|
||||
user: postgres
|
||||
password: ''
|
||||
password: postgres
|
||||
#pool_size: 10
|
||||
#max_retries: 0
|
||||
#log_level: "debug"
|
||||
|
Reference in New Issue
Block a user