Compare commits

...

12 Commits

64 changed files with 2248 additions and 1345 deletions

5
.gitignore vendored
View File

@ -27,8 +27,11 @@
main main
.DS_Store .DS_Store
.swp .swp
.release
main main
super-graph super-graph
supergraph
*-fuzz.zip *-fuzz.zip
crashers crashers
suppressions suppressions
release

View File

@ -6,13 +6,13 @@ RUN yarn
RUN yarn build RUN yarn build
# stage: 2 # stage: 2
FROM golang:1.13beta1-alpine as go-build FROM golang:1.13.4-alpine as go-build
RUN apk update && \ RUN apk update && \
apk add --no-cache make && \
apk add --no-cache git && \ apk add --no-cache git && \
apk add --no-cache upx=3.95-r2 apk add --no-cache upx=3.95-r2
RUN go get -u github.com/rafaelsq/wtc && \ RUN go get -u github.com/rafaelsq/wtc
go get -u github.com/GeertJohan/go.rice/rice
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
@ -20,11 +20,9 @@ COPY . /app
RUN mkdir -p /app/web/build RUN mkdir -p /app/web/build
COPY --from=react-build /web/build/ ./web/build/ COPY --from=react-build /web/build/ ./web/build/
ENV GO111MODULE=on
RUN go mod vendor RUN go mod vendor
RUN go generate ./... && \ RUN make build
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o super-graph && \ RUN echo "Compressing binary, will take a bit of time..." && \
echo "Compressing binary, will take a bit of time..." && \
upx --ultra-brute -qq super-graph && \ upx --ultra-brute -qq super-graph && \
upx -t super-graph upx -t super-graph

189
LICENSE
View File

@ -1,21 +1,176 @@
The MIT License (MIT) Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2019-present Vikram Rangnekar. twitter.com/dosco TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Permission is hereby granted, free of charge, to any person obtaining a copy 1. Definitions.
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in "License" shall mean the terms and conditions for use, reproduction,
all copies or substantial portions of the Software. and distribution as defined by Sections 1 through 9 of this document.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR "Licensor" shall mean the copyright owner or entity authorized by
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, the copyright owner that is granting the License.
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER "Legal Entity" shall mean the union of the acting entity and all
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, other entities that control, are controlled by, or are under common
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN control with that entity. For the purposes of this definition,
THE SOFTWARE. "control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

93
Makefile Normal file
View File

@ -0,0 +1,93 @@
BUILD ?= $(shell git rev-parse --short HEAD)
BUILD_DATE ?= $(shell git log -1 --format=%ci)
BUILD_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
BUILD_VERSION ?= $(shell git describe --always --tags)
PKGS := $(shell go list ./... | grep -v /vendor)
GOPATH ?= $(shell go env GOPATH)
ifndef GOPATH
override GOPATH = $(HOME)/go
endif
export GO111MODULE := on
# Build-time Go variables
version = github.com/dosco/super-graph/serv.version
gitBranch = github.com/dosco/super-graph/serv.gitBranch
lastCommitSHA = github.com/dosco/super-graph/serv.lastCommitSHA
lastCommitTime = github.com/dosco/super-graph/serv.lastCommitTime
BUILD_FLAGS ?= -ldflags '-s -w -X ${lastCommitSHA}=${BUILD} -X "${lastCommitTime}=${BUILD_DATE}" -X "${version}=${BUILD_VERSION}" -X ${gitBranch}=${BUILD_BRANCH}'
.PHONY: all build gen clean test run lint release version help $(PLATFORMS) $(BINARY)
test: lint
@go test -v $(PKGS)
BIN_DIR := $(GOPATH)/bin
GOLANGCILINT := $(BIN_DIR)/golangci-lint
$(GOLANGCILINT):
@curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(GOPATH)/bin v1.21.0
lint: $(GOMETALINTER)
@golangci-lint run ./... --skip-dirs-use-default
BINARY := super-graph
LDFLAGS := -s -w
PLATFORMS := windows linux darwin
os = $(word 1, $@)
$(PLATFORMS): gen
@mkdir -p release
@GOOS=$(os) GOARCH=amd64 go build $(BUILD_FLAGS) -o release/$(BINARY)-$(BUILD_VERSION)-$(os)-amd64
release: windows linux darwin
all: $(BINARY)
build: $(BINARY)
gen:
@go install github.com/GeertJohan/go.rice/rice
@go generate ./...
$(BINARY): clean gen
@go build $(BUILD_FLAGS) -o $(BINARY)
clean:
@rm -f $(BINARY)
run: clean
@go run $(BUILD_FLAGS) main.go $(ARGS)
install: gen
@echo $(GOPATH)
@echo "Commit Hash: `git rev-parse HEAD`"
@echo "Old Hash: `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`"
@go install $(BUILD_FLAGS)
@echo "New Hash:" `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`
uninstall: clean
@go clean -i -x
version:
@echo Super Graph ${BUILD_VERSION}
@echo Build: ${BUILD}
@echo Build date: ${BUILD_DATE}
@echo Branch: ${BUILD_BRANCH}
@echo Go version: $(shell go version)
help:
@echo
@echo Build commands:
@echo " make build - Build supergraph binary"
@echo " make install - Install supergraph binary"
@echo " make uninstall - Uninstall supergraph binary"
@echo " make [platform] - Build for platform [linux|darwin|windows]"
@echo " make release - Build all platforms"
@echo " make run - Run supergraph (eg. make run ARGS=\"version\")"
@echo " make version - Show current build info"
@echo " make help - This help"
@echo

13
NOTICE Normal file
View File

@ -0,0 +1,13 @@
Copyright 2019 Vikram Rangnekar
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,10 +1,10 @@
<a href="https://supergraph.dev"><img src="https://supergraph.dev/hologram.svg" width="100" height="100" align="right" /></a> <a href="https://supergraph.dev"><img src="https://supergraph.dev/hologram.svg" width="100" height="100" align="right" /></a>
# Super Graph - Instant GraphQL APIs for your apps. # Super Graph
## Build web products faster. No code needed. GraphQL auto. transformed into efficient database queries. ### Build web products faster. Instant GraphQL API without writing any code. Works with Postgres. Also supports Rails apps.
![MIT license](https://img.shields.io/github/license/dosco/super-graph.svg) ![Apache Public License 2.0](https://img.shields.io/github/license/dosco/super-graph.svg)
![Docker build](https://img.shields.io/docker/cloud/build/dosco/super-graph.svg) ![Docker build](https://img.shields.io/docker/cloud/build/dosco/super-graph.svg)
![Cloud native](https://img.shields.io/badge/cloud--native-enabled-blue.svg) ![Cloud native](https://img.shields.io/badge/cloud--native-enabled-blue.svg)
[![Discord Chat](https://img.shields.io/discord/628796009539043348.svg)](https://discord.gg/6pSWCTZ) [![Discord Chat](https://img.shields.io/discord/628796009539043348.svg)](https://discord.gg/6pSWCTZ)
@ -56,7 +56,7 @@ Twitter or Discord.
## License ## License
[MIT](http://opensource.org/licenses/MIT) [Apache Public License 2.0](https://opensource.org/licenses/Apache-2.0)
Copyright (c) 2019-present Vikram Rangnekar Copyright (c) 2019-present Vikram Rangnekar

View File

@ -73,43 +73,6 @@ mutation {
} }
} }
variables {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query {
products {
id
name
}
}
variables {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query { query {
products { products {
id id
@ -133,21 +96,6 @@ mutation {
} }
} }
variables {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query { query {
products { products {
id id
@ -174,39 +122,6 @@ mutation {
} }
} }
variables {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query {
products {
id
name
users {
email
}
}
}
query {
me {
id
email
full_name
}
}
variables { variables {
"update": { "update": {
"name": "Helloo", "name": "Helloo",
@ -224,66 +139,25 @@ mutation {
} }
variables { variables {
"data": [ "data": {
{ "name": "WOOO",
"name": "Gumbo1", "price": 50.5
"created_at": "now", }
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
} }
query { mutation {
product { products(insert: $data) {
id id
name name
} }
} }
variables { query getProducts {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query {
products { products {
id id
name name
price
description description
users {
email
}
}
}
query {
users {
id
email
picture: avatar
password
full_name
products(limit: 2, where: {price: {gt: 10}}) {
id
name
description
price
}
} }
} }

View File

@ -9,7 +9,7 @@ log_level: "debug"
# from the allow list are permitted. # from the allow list are permitted.
# When it's 'false' all queries are saved to the # When it's 'false' all queries are saved to the
# the allow list in ./config/allow.list # the allow list in ./config/allow.list
production: true production: false
# Throw a 401 on auth failure for queries that need auth # Throw a 401 on auth failure for queries that need auth
auth_fail_block: false auth_fail_block: false
@ -97,23 +97,18 @@ database:
# Enable this if you need the user id in triggers, etc # Enable this if you need the user id in triggers, etc
set_user_id: false set_user_id: false
# Define variables here that you want to use in filters # Define additional variables here to be used with filters
# sub-queries must be wrapped in ()
variables: variables:
account_id: "(select account_id from users where id = $user_id)" admin_account_id: "5"
# Define defaults to for the field key and values below # Field and table names that you wish to block
defaults: blocklist:
# filters: ["{ user_id: { eq: $user_id } }"] - ar_internal_metadata
- schema_migrations
# Field and table names that you wish to block - secret
blocklist: - password
- ar_internal_metadata - encrypted
- schema_migrations - token
- secret
- password
- encrypted
- token
tables: tables:
- name: customers - name: customers
@ -141,6 +136,7 @@ roles_query: "SELECT * FROM users WHERE id = $user_id"
roles: roles:
- name: anon - name: anon
tables: tables:
- name: users
- name: products - name: products
limit: 10 limit: 10
@ -174,8 +170,10 @@ roles:
filters: ["{ user_id: { eq: $user_id } }"] filters: ["{ user_id: { eq: $user_id } }"]
columns: ["id", "name", "description" ] columns: ["id", "name", "description" ]
presets: presets:
- user_id: "$user_id"
- created_at: "now" - created_at: "now"
- updated_at: "now"
update: update:
filters: ["{ user_id: { eq: $user_id } }"] filters: ["{ user_id: { eq: $user_id } }"]
columns: columns:
@ -188,8 +186,7 @@ roles:
block: true block: true
- name: admin - name: admin
match: id = 1 match: id = 1000
tables: tables:
- name: users - name: users
# query: filters: []
# filters: ["{ account_id: { _eq: $account_id } }"]

View File

@ -41,40 +41,6 @@ enable_tracing: true
# SG_AUTH_RAILS_REDIS_PASSWORD # SG_AUTH_RAILS_REDIS_PASSWORD
# SG_AUTH_JWT_PUBLIC_KEY_FILE # SG_AUTH_JWT_PUBLIC_KEY_FILE
# inflections:
# person: people
# sheep: sheep
auth:
# Can be 'rails' or 'jwt'
type: rails
cookie: _app_session
rails:
# Rails version this is used for reading the
# various cookies formats.
version: 5.2
# Found in 'Rails.application.config.secret_key_base'
secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566
# Remote cookie store. (memcache or redis)
# url: redis://127.0.0.1:6379
# password: test
# max_idle: 80,
# max_active: 12000,
# In most cases you don't need these
# salt: "encrypted cookie"
# sign_salt: "signed encrypted cookie"
# auth_salt: "authenticated encrypted cookie"
# jwt:
# provider: auth0
# secret: abc335bfcfdb04e50db5bb0a4d67ab9
# public_key_file: /secrets/public_key.pem
# public_key_type: ecdsa #rsa
database: database:
type: postgres type: postgres
host: db host: db

View File

@ -46,10 +46,10 @@ for (i = 0; i < product_count; i++) {
var data = { var data = {
name: fake.beer_name(), name: fake.beer_name(),
description: desc, description: desc,
price: fake.price(), price: fake.price()
user_id: user.id, //user_id: user.id,
created_at: "now", //created_at: "now",
updated_at: "now" //updated_at: "now"
} }
var res = graphql(" \ var res = graphql(" \
@ -57,7 +57,9 @@ for (i = 0; i < product_count; i++) {
product(insert: $data) { \ product(insert: $data) { \
id \ id \
} \ } \
}", { data: data }) }", { data: data }, {
user_id: 5
})
products.push(res.product) products.push(res.product)
} }

View File

@ -26,7 +26,7 @@ module.exports = {
['meta', { prefix: ogprefix, property: 'og:title', content: title }], ['meta', { prefix: ogprefix, property: 'og:title', content: title }],
['meta', { prefix: ogprefix, property: 'twitter:title', content: title }], ['meta', { prefix: ogprefix, property: 'twitter:title', content: title }],
['meta', { prefix: ogprefix, property: 'og:type', content: 'website' }], ['meta', { prefix: ogprefix, property: 'og:type', content: 'website' }],
['meta', { prefix: ogprefix, property: 'og:url', content: 'https://supergraph.dev }], ['meta', { prefix: ogprefix, property: 'og:url', content: 'https://supergraph.dev' }],
['meta', { prefix: ogprefix, property: 'og:description', content: description }], ['meta', { prefix: ogprefix, property: 'og:description', content: description }],
//['meta', { prefix: ogprefix, property: 'og:image', content: 'https://wireupyourfrontend.com/assets/logo.png' }], //['meta', { prefix: ogprefix, property: 'og:image', content: 'https://wireupyourfrontend.com/assets/logo.png' }],
// ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], // ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],

View File

@ -26,5 +26,5 @@ features:
- title: Free and Open Source - title: Free and Open Source
details: Not a VC funded startup. Not even a startup just good old open source code details: Not a VC funded startup. Not even a startup just good old open source code
footer: MIT Licensed | Copyright © 2018-present Vikram Rangnekar footer: Apache Public License 2.0 | Copyright © 2018-present Vikram Rangnekar
--- ---

View File

@ -147,9 +147,13 @@ Super Graph can generate your initial app for you. The generated app will have c
You can then add your database schema to the migrations, maybe create some seed data using the seed script and launch Super Graph. You're now good to go and can start working on your UI frontend in React, Vue or whatever. You can then add your database schema to the migrations, maybe create some seed data using the seed script and launch Super Graph. You're now good to go and can start working on your UI frontend in React, Vue or whatever.
```bash ```bash
# use the below command to download and install Super Graph. You will need Go 1.13 or above # Download and install Super Graph. You will need Go 1.13 or above
GO111MODULE=on go get -u github.com/dosco/super-graph git clone https://github.com/dosco/super-graph && cd super-graph && make install
```
And then create and launch you're new app
```bash
# create a new app and change to it's directory # create a new app and change to it's directory
super-graph new blog; cd blog super-graph new blog; cd blog
@ -276,7 +280,7 @@ transmission_gear_type
// Text // Text
word word
sentence sentence
paragrph paragraph
question question
quote quote
@ -476,6 +480,21 @@ query {
} }
``` ```
Multiple tables can also be fetched using a single GraphQL query. This is very fast since the entire query is converted into a single SQL query which the database can efficiently run.
```graphql
query {
user {
full_name
email
}
products {
name
description
}
}
```
### Fetching data ### Fetching data
To fetch a specific `product` by it's ID you can use the `id` argument. The real name id field will be resolved automatically so this query will work even if your id column is named something like `product_id`. To fetch a specific `product` by it's ID you can use the `id` argument. The real name id field will be resolved automatically so this query will work even if your id column is named something like `product_id`.
@ -908,6 +927,40 @@ class AddSearchColumn < ActiveRecord::Migration[5.1]
end end
``` ```
## GraphQL with React
This is a quick simple example using `graphql.js` [https://github.com/f/graphql.js/](https://github.com/f/graphql.js/)
```js
import React, { useState, useEffect } from 'react'
import graphql from 'graphql.js'
// Create a GraphQL client pointing to Super Graph
var graph = graphql("http://localhost:3000/api/v1/graphql", { asJSON: true })
const App = () => {
const [user, setUser] = useState(null)
useEffect(() => {
async function action() {
// Use the GraphQL client to execute a graphQL query
// The second argument to the client are the variables you need to pass
const result = await graph(`{ user { id first_name last_name picture_url } }`)()
setUser(result)
}
action()
}, []);
return (
<div className="App">
<h1>{ JSON.stringify(user) }</h1>
</div>
);
}
```
export default App;
## Authentication ## Authentication
You can only have one type of auth enabled. You can either pick Rails or JWT. You can only have one type of auth enabled. You can either pick Rails or JWT.
@ -1034,25 +1087,6 @@ must be run to help figure out a users role. This query can be as complex as you
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` willbe assigned the admin role and those that don't match get the `user` role if authenticated successfully or the `anon` role.
This below example would work for SAAS apps where an account (tenant) is usually the top parent table to everything else.
```yaml
roles_query: "SELECT * FROM users JOIN accounts on accounts.id = users.account_id WHERE users.id = $user_id"
roles:
- name: user
tables:
- name: users
...
- name: admin
match: accounts.admin_id = $user_id
tables:
- name: users
query:
filters: [{ accounts: { id: { eq: $account_id } } }]
```
## Remote Joins ## Remote Joins
It often happens that after fetching some data from the DB we need to call another API to fetch some more data and all this combined into a single JSON response. For example along with a list of users you need their last 5 payments from Stripe. This requires you to query your DB for the users and Stripe for the payments. Super Graph handles all this for you also only the fields you requested from the Stripe API are returned. It often happens that after fetching some data from the DB we need to call another API to fetch some more data and all this combined into a single JSON response. For example along with a list of users you need their last 5 payments from Stripe. This requires you to query your DB for the users and Stripe for the payments. Super Graph handles all this for you also only the fields you requested from the Stripe API are returned.
@ -1241,23 +1275,18 @@ database:
# Enable this if you need the user id in triggers, etc # Enable this if you need the user id in triggers, etc
set_user_id: false set_user_id: false
# Define variables here that you want to use in filters # Define additional variables here to be used with filters
# sub-queries must be wrapped in ()
variables: variables:
account_id: "(select account_id from users where id = $user_id)" admin_account_id: "5"
# Define defaults to for the field key and values below # Field and table names that you wish to block
defaults: blocklist:
# filters: ["{ user_id: { eq: $user_id } }"] - ar_internal_metadata
- schema_migrations
# Field and table names that you wish to block - secret
blocklist: - password
- ar_internal_metadata - encrypted
- schema_migrations - token
- secret
- password
- encrypted
- token
tables: tables:
- name: customers - name: customers
@ -1329,14 +1358,13 @@ roles:
- updated_at: "now" - updated_at: "now"
delete: delete:
deny: true block: true
- name: admin - name: admin
match: id = 1 match: id = 1000
tables: tables:
- name: users - name: users
# query: filters: []
# filters: ["{ account_id: { _eq: $account_id } }"]
``` ```
@ -1372,9 +1400,6 @@ brew install yarn
# yarn install dependencies and build the web ui # yarn install dependencies and build the web ui
(cd web && yarn install && yarn build) (cd web && yarn install && yarn build)
# generate some stuff the go code needs
go generate ./...
# do this the only the time to setup the database # do this the only the time to setup the database
docker-compose run rails_app rake db:create db:migrate db:seed docker-compose run rails_app rake db:create db:migrate db:seed
@ -1383,6 +1408,6 @@ docker-compose up
``` ```
## MIT License ## Apache License 2.0
MIT Licensed | Copyright © 2018-present Vikram Rangnekar Apache Public License 2.0 | Copyright © 2018-present Vikram Rangnekar

4
go.sum
View File

@ -1,6 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= 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 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
@ -13,6 +14,7 @@ github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 h1:+qz9Ga6l6lKw6fgvk5RMV5HQznSLvI8Zxajwdj4FhFg= github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 h1:+qz9Ga6l6lKw6fgvk5RMV5HQznSLvI8Zxajwdj4FhFg=
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3/go.mod h1:FlkD11RtgMTYjVuBnb7cxoHmQGqvPpCsr2atC88nl/M= github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3/go.mod h1:FlkD11RtgMTYjVuBnb7cxoHmQGqvPpCsr2atC88nl/M=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -140,6 +142,7 @@ github.com/jackc/puddle v1.0.0 h1:rbjAshlgKscNa7j0jAM0uNQflis5o2XUogPMVAwtcsM=
github.com/jackc/puddle v1.0.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.0.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/tern v1.8.2 h1:+d9eK83fRS0dbf6nt+2tjILYF4FKG1O5xTFB8Lzc66U= github.com/jackc/tern v1.8.2 h1:+d9eK83fRS0dbf6nt+2tjILYF4FKG1O5xTFB8Lzc66U=
github.com/jackc/tern v1.8.2/go.mod h1:AMppp2oyCT6rYnJHLLMmPWwahfFvdIVi6mr9gH81Nxs= github.com/jackc/tern v1.8.2/go.mod h1:AMppp2oyCT6rYnJHLLMmPWwahfFvdIVi6mr9gH81Nxs=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@ -173,6 +176,7 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=

View File

@ -139,7 +139,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
} }
if sk > 0 && sk < len(cb) { if sk > 0 && sk < len(cb) {
_, err = w.Write(cb[sk:len(cb)]) _, err = w.Write(cb[sk:])
} else { } else {
_, err = w.Write(cb) _, err = w.Write(cb)
} }

View File

@ -55,6 +55,6 @@ func TestFuzzCrashers(t *testing.T) {
} }
for _, f := range crashers { for _, f := range crashers {
unifiedTest([]byte(f)) _ = unifiedTest([]byte(f))
} }
} }

View File

@ -191,11 +191,11 @@ func TestGet(t *testing.T) {
} }
for i := range expected { for i := range expected {
if bytes.Equal(values[i].Key, expected[i].Key) == false { if !bytes.Equal(values[i].Key, expected[i].Key) {
t.Error(string(values[i].Key), " != ", string(expected[i].Key)) t.Error(string(values[i].Key), " != ", string(expected[i].Key))
} }
if bytes.Equal(values[i].Value, expected[i].Value) == false { if !bytes.Equal(values[i].Value, expected[i].Value) {
t.Error(string(values[i].Value), " != ", string(expected[i].Value)) t.Error(string(values[i].Value), " != ", string(expected[i].Value))
} }
} }
@ -225,7 +225,10 @@ func TestValue(t *testing.T) {
func TestFilter1(t *testing.T) { func TestFilter1(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
Filter(&b, []byte(input2), []string{"id", "full_name", "embed"}) err := Filter(&b, []byte(input2), []string{"id", "full_name", "embed"})
if err != nil {
t.Error(err)
}
expected := `[{"id": 1,"full_name": "Sidney Stroman","embed": {"id": 8,"full_name": "Caroll Orn Sr.","email": "joannarau@hegmann.io","__twitter_id": "ABC123"}},{"id": 2,"full_name": "Jerry Dickinson"}]` expected := `[{"id": 1,"full_name": "Sidney Stroman","embed": {"id": 8,"full_name": "Caroll Orn Sr.","email": "joannarau@hegmann.io","__twitter_id": "ABC123"}},{"id": 2,"full_name": "Jerry Dickinson"}]`
@ -238,7 +241,10 @@ func TestFilter2(t *testing.T) {
value := `[{"id":1,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":100,"amount_refunded":0,"date":"01/01/2019","application":null,"billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}}, {"id":2,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":150,"amount_refunded":0,"date":"02/18/2019","billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}},{"id":3,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":150,"amount_refunded":50,"date":"03/21/2019","billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}}]` value := `[{"id":1,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":100,"amount_refunded":0,"date":"01/01/2019","application":null,"billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}}, {"id":2,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":150,"amount_refunded":0,"date":"02/18/2019","billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}},{"id":3,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":150,"amount_refunded":50,"date":"03/21/2019","billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}}]`
var b bytes.Buffer var b bytes.Buffer
Filter(&b, []byte(value), []string{"id"}) err := Filter(&b, []byte(value), []string{"id"})
if err != nil {
t.Error(err)
}
expected := `[{"id":1},{"id":2},{"id":3}]` expected := `[{"id":1},{"id":2},{"id":3}]`
@ -253,7 +259,7 @@ func TestStrip(t *testing.T) {
expected := []byte(`[{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]`) expected := []byte(`[{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]`)
if bytes.Equal(value1, expected) == false { if !bytes.Equal(value1, expected) {
t.Log(value1) t.Log(value1)
t.Error("[Valid path] Does not match expected json") t.Error("[Valid path] Does not match expected json")
} }
@ -261,7 +267,7 @@ func TestStrip(t *testing.T) {
path2 := [][]byte{[]byte("boo"), []byte("hoo")} path2 := [][]byte{[]byte("boo"), []byte("hoo")}
value2 := Strip([]byte(input3), path2) value2 := Strip([]byte(input3), path2)
if bytes.Equal(value2, []byte(input3)) == false { if !bytes.Equal(value2, []byte(input3)) {
t.Log(value2) t.Log(value2)
t.Error("[Invalid path] Does not match expected json") t.Error("[Invalid path] Does not match expected json")
} }
@ -374,10 +380,6 @@ func TestKeys2(t *testing.T) {
"id", "posts", "title", "description", "full_name", "email", "books", "name", "description", "id", "posts", "title", "description", "full_name", "email", "books", "name", "description",
} }
// for i := range fields {
// fmt.Println("-->", string(fields[i]))
// }
if len(exp) != len(fields) { if len(exp) != len(fields) {
t.Errorf("Expected %d fields %d", len(exp), len(fields)) t.Errorf("Expected %d fields %d", len(exp), len(fields))
} }

View File

@ -16,8 +16,12 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
tmap := make(map[uint64]int, len(from)) tmap := make(map[uint64]int, len(from))
for i, f := range from { for i, f := range from {
h.Write(f.Key) if _, err := h.Write(f.Key); err != nil {
h.Write(f.Value) return err
}
if _, err := h.Write(f.Value); err != nil {
return err
}
tmap[h.Sum64()] = i tmap[h.Sum64()] = i
h.Reset() h.Reset()
@ -50,7 +54,9 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
case state == expectKeyClose && b[i] == '"': case state == expectKeyClose && b[i] == '"':
state = expectColon state = expectColon
h.Write(b[(s + 1):i]) if _, err := h.Write(b[(s + 1):i]); err != nil {
return err
}
we = s we = s
case state == expectColon && b[i] == ':': case state == expectColon && b[i] == ':':
@ -106,7 +112,9 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
if e != 0 { if e != 0 {
e++ e++
h.Write(b[s:e]) if _, err := h.Write(b[s:e]); err != nil {
return err
}
n, ok := tmap[h.Sum64()] n, ok := tmap[h.Sum64()]
h.Reset() h.Reset()

View File

@ -2,7 +2,6 @@ package jsn
import ( import (
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"strings" "strings"
"unsafe" "unsafe"
@ -333,15 +332,6 @@ func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b)) return *(*string)(unsafe.Pointer(&b))
} }
func s2b(s string) []byte {
strh := (*reflect.StringHeader)(unsafe.Pointer(&s))
var sh reflect.SliceHeader
sh.Data = strh.Data
sh.Len = strh.Len
sh.Cap = strh.Len
return *(*[]byte)(unsafe.Pointer(&sh))
}
const maxStartEndStringLen = 80 const maxStartEndStringLen = 80
func startEndString(s string) string { func startEndString(s string) string {

View File

@ -244,7 +244,6 @@ func (m *Migrator) AppendMigration(name, upSQL, downSQL string) {
UpSQL: upSQL, UpSQL: upSQL,
DownSQL: downSQL, DownSQL: downSQL,
}) })
return
} }
// Migrate runs pending migrations // Migrate runs pending migrations
@ -315,7 +314,7 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
if err != nil { if err != nil {
return err return err
} }
defer tx.Rollback(ctx) defer tx.Rollback(ctx) //nolint: errcheck
// Fire on start callback // Fire on start callback
if m.OnStart != nil { if m.OnStart != nil {
@ -332,7 +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. // Reset all database connection settings. Important to do before updating version as search_path may have been changed.
tx.Exec(ctx, "reset all") if _, err := tx.Exec(ctx, "reset all"); err != nil {
return err
}
// Add one to the version // Add one to the version
_, err = tx.Exec(ctx, "update "+m.versionTable+" set version=$1", sequence) _, err = tx.Exec(ctx, "update "+m.versionTable+" set version=$1", sequence)

View File

@ -1,3 +1,4 @@
//nolint:errcheck
package psql package psql
import ( import (
@ -77,9 +78,9 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer,
return 0, err return 0, err
} }
io.WriteString(c.w, `(WITH "input" AS (SELECT {{`) io.WriteString(c.w, `(WITH "input" AS (SELECT '{{`)
io.WriteString(c.w, qc.ActionVar) io.WriteString(c.w, qc.ActionVar)
io.WriteString(c.w, `}}::json AS j) INSERT INTO `) io.WriteString(c.w, `}}' :: json AS j) INSERT INTO `)
quoted(c.w, ti.Name) quoted(c.w, ti.Name)
io.WriteString(c.w, ` (`) io.WriteString(c.w, ` (`)
c.renderInsertUpdateColumns(qc, w, jt, ti, false) c.renderInsertUpdateColumns(qc, w, jt, ti, false)
@ -174,9 +175,9 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer,
return 0, err return 0, err
} }
io.WriteString(c.w, `(WITH "input" AS (SELECT {{`) io.WriteString(c.w, `(WITH "input" AS (SELECT '{{`)
io.WriteString(c.w, qc.ActionVar) io.WriteString(c.w, qc.ActionVar)
io.WriteString(c.w, `}}::json AS j) UPDATE `) io.WriteString(c.w, `}}' :: json AS j) UPDATE `)
quoted(c.w, ti.Name) quoted(c.w, ti.Name)
io.WriteString(c.w, ` SET (`) io.WriteString(c.w, ` SET (`)
c.renderInsertUpdateColumns(qc, w, jt, ti, false) c.renderInsertUpdateColumns(qc, w, jt, ti, false)

View File

@ -12,7 +12,7 @@ func simpleInsert(t *testing.T) {
} }
}` }`
sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM 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{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
@ -36,7 +36,7 @@ func singleInsert(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" ("name", "description", "user_id") SELECT "name", "description", "user_id" FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (WITH "input" AS (SELECT '{{insert}}' :: json AS j) INSERT INTO "products" ("name", "description", "user_id") SELECT "name", "description", "user_id" FROM 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{ vars := map[string]json.RawMessage{
"insert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc", "user_id": 5 }`), "insert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc", "user_id": 5 }`),
@ -60,7 +60,7 @@ func bulkInsert(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (WITH "input" AS (SELECT '{{insert}}' :: json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM 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{ vars := map[string]json.RawMessage{
"insert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`), "insert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`),
@ -84,7 +84,7 @@ func singleUpsert(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (WITH "input" AS (SELECT '{{upsert}}' :: json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t 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{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
@ -108,7 +108,7 @@ func singleUpsertWhere(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT (id) WHERE (("products"."price") > 3) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (WITH "input" AS (SELECT '{{upsert}}' :: json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t 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{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
@ -132,7 +132,7 @@ func bulkUpsert(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (WITH "input" AS (SELECT '{{upsert}}' :: json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t 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{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`), "upsert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`),
@ -156,7 +156,7 @@ func singleUpdate(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{update}}::json AS j) UPDATE "products" SET ("name", "description") = (SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 1) AND (("products"."id") = 15) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (WITH "input" AS (SELECT '{{update}}' :: json AS j) UPDATE "products" SET ("name", "description") = (SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 1) AND (("products"."id") = 15) 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{ vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
@ -180,7 +180,7 @@ func delete(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (DELETE FROM "products" WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (DELETE FROM "products" WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) 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{ vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
@ -203,7 +203,7 @@ func blockedInsert(t *testing.T) {
} }
}` }`
sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false 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{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
@ -227,7 +227,7 @@ func blockedUpdate(t *testing.T) {
} }
}` }`
sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id", "users"."email" FROM "users") AS "users_0") AS "done_1337"` sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false 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"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
@ -250,7 +250,7 @@ func simpleInsertWithPresets(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "name", "price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '$user_id' :: bigint FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "name", "price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`), "data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`),
@ -273,7 +273,7 @@ func simpleUpdateWithPresets(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "name", "price", 'now' :: timestamp without time zone FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = {{user_id}}) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"` sql := `WITH "products" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "name", "price", 'now' :: timestamp without time zone FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = '{{user_id}}' :: bigint) 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{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`), "data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),

View File

@ -27,8 +27,11 @@ func TestMain(m *testing.M) {
"token", "token",
}, },
}) })
if err != nil {
log.Fatal(err)
}
qcompile.AddRole("user", "product", qcode.TRConfig{ err = qcompile.AddRole("user", "product", qcode.TRConfig{
Query: qcode.QueryConfig{ Query: qcode.QueryConfig{
Columns: []string{"id", "name", "price", "users", "customers"}, Columns: []string{"id", "name", "price", "users", "customers"},
Filters: []string{ Filters: []string{
@ -54,27 +57,39 @@ func TestMain(m *testing.M) {
}, },
}, },
}) })
if err != nil {
log.Fatal(err)
}
qcompile.AddRole("anon", "product", qcode.TRConfig{ err = qcompile.AddRole("anon", "product", qcode.TRConfig{
Query: qcode.QueryConfig{ Query: qcode.QueryConfig{
Columns: []string{"id", "name"}, Columns: []string{"id", "name"},
}, },
}) })
if err != nil {
log.Fatal(err)
}
qcompile.AddRole("anon1", "product", qcode.TRConfig{ err = qcompile.AddRole("anon1", "product", qcode.TRConfig{
Query: qcode.QueryConfig{ Query: qcode.QueryConfig{
Columns: []string{"id", "name", "price"}, Columns: []string{"id", "name", "price"},
DisableFunctions: true, DisableFunctions: true,
}, },
}) })
if err != nil {
log.Fatal(err)
}
qcompile.AddRole("user", "users", qcode.TRConfig{ err = qcompile.AddRole("user", "users", qcode.TRConfig{
Query: qcode.QueryConfig{ Query: qcode.QueryConfig{
Columns: []string{"id", "full_name", "avatar", "email", "products"}, Columns: []string{"id", "full_name", "avatar", "email", "products"},
}, },
}) })
if err != nil {
log.Fatal(err)
}
qcompile.AddRole("bad_dude", "users", qcode.TRConfig{ err = qcompile.AddRole("bad_dude", "users", qcode.TRConfig{
Query: qcode.QueryConfig{ Query: qcode.QueryConfig{
Filters: []string{"false"}, Filters: []string{"false"},
DisableFunctions: true, DisableFunctions: true,
@ -86,8 +101,11 @@ func TestMain(m *testing.M) {
Filters: []string{"false"}, Filters: []string{"false"},
}, },
}) })
if err != nil {
log.Fatal(err)
}
qcompile.AddRole("user", "mes", qcode.TRConfig{ err = qcompile.AddRole("user", "mes", qcode.TRConfig{
Query: qcode.QueryConfig{ Query: qcode.QueryConfig{
Columns: []string{"id", "full_name", "avatar"}, Columns: []string{"id", "full_name", "avatar"},
Filters: []string{ Filters: []string{
@ -95,8 +113,11 @@ func TestMain(m *testing.M) {
}, },
}, },
}) })
if err != nil {
log.Fatal(err)
}
qcompile.AddRole("user", "customers", qcode.TRConfig{ err = qcompile.AddRole("user", "customers", qcode.TRConfig{
Query: qcode.QueryConfig{ Query: qcode.QueryConfig{
Columns: []string{"id", "email", "full_name", "products"}, Columns: []string{"id", "email", "full_name", "products"},
}, },
@ -167,11 +188,13 @@ func TestMain(m *testing.M) {
} }
for i, t := range tables { for i, t := range tables {
schema.updateSchema(t, columns[i], aliases) if err := schema.updateSchema(t, columns[i], aliases); err != nil {
log.Fatal(err)
}
} }
vars := NewVariables(map[string]string{ vars := NewVariables(map[string]string{
"account_id": "select account_id from users where id = $user_id", "admin_account_id": "5",
}) })
pcompile = NewCompiler(Config{ pcompile = NewCompiler(Config{

View File

@ -1,3 +1,4 @@
//nolint:errcheck
package psql package psql
import ( import (
@ -6,10 +7,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"strings" "strings"
"github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
"github.com/dosco/super-graph/util" "github.com/dosco/super-graph/util"
) )
@ -77,30 +76,49 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
} }
c := &compilerContext{w, qc.Selects, co} c := &compilerContext{w, qc.Selects, co}
root := &qc.Selects[0] multiRoot := (len(qc.Roots) > 1)
ti, err := c.schema.GetTable(root.Table)
if err != nil {
return 0, err
}
st := NewStack() st := NewStack()
st.Push(root.ID + closeBlock)
st.Push(root.ID)
//fmt.Fprintf(w, `SELECT json_object_agg('%s', %s) FROM (`, if multiRoot {
//root.FieldName, root.Table) io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `)
io.WriteString(c.w, `SELECT json_object_agg('`)
io.WriteString(c.w, root.FieldName) for i, id := range qc.Roots {
io.WriteString(c.w, `', `) root := qc.Selects[id]
st.Push(root.ID + closeBlock)
st.Push(root.ID)
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)
}
io.WriteString(c.w, ` FROM `)
if ti.Singular == false {
io.WriteString(c.w, root.Table)
} else { } else {
io.WriteString(c.w, "sel_json_") root := qc.Selects[0]
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) int2string(c.w, root.ID)
st.Push(root.ID + closeBlock)
st.Push(root.ID)
io.WriteString(c.w, `) FROM `)
} }
io.WriteString(c.w, `) FROM (`)
var ignored uint32 var ignored uint32
@ -114,16 +132,21 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
if id < closeBlock { if id < closeBlock {
sel := &c.s[id] sel := &c.s[id]
if sel.ParentID == -1 {
io.WriteString(c.w, `(`)
}
ti, err := c.schema.GetTable(sel.Table) ti, err := c.schema.GetTable(sel.Table)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if sel.ID != 0 { if sel.ParentID != -1 {
if err = c.renderLateralJoin(sel); err != nil { if err = c.renderLateralJoin(sel); err != nil {
return 0, err return 0, err
} }
} }
skipped, err := c.renderSelect(sel, ti) skipped, err := c.renderSelect(sel, ti)
if err != nil { if err != nil {
return 0, err return 0, err
@ -153,16 +176,25 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
return 0, err return 0, err
} }
if sel.ID != 0 { if sel.ParentID != -1 {
if err = c.renderLateralJoinClose(sel); err != nil { if err = c.renderLateralJoinClose(sel); err != nil {
return 0, err return 0, err
} }
} else {
io.WriteString(c.w, `)`)
aliasWithID(c.w, `sel`, sel.ID)
if st.Len() != 0 {
io.WriteString(c.w, `, `)
}
} }
} }
} }
io.WriteString(c.w, `)`) if multiRoot {
alias(c.w, `done_1337`) io.WriteString(c.w, `) AS "json_root"`)
}
return ignored, nil return ignored, nil
} }
@ -216,10 +248,10 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
hasOrder := len(sel.OrderBy) != 0 hasOrder := len(sel.OrderBy) != 0
// SELECT // SELECT
if ti.Singular == false { if !ti.Singular {
//fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Table) //fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Table)
io.WriteString(c.w, `SELECT coalesce(json_agg("`) io.WriteString(c.w, `SELECT coalesce(json_agg("`)
io.WriteString(c.w, "sel_json_") io.WriteString(c.w, "json_")
int2string(c.w, sel.ID) int2string(c.w, sel.ID)
io.WriteString(c.w, `"`) io.WriteString(c.w, `"`)
@ -232,7 +264,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
//fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Table) //fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Table)
io.WriteString(c.w, `), '[]')`) io.WriteString(c.w, `), '[]')`)
alias(c.w, sel.Table) aliasWithID(c.w, "json", sel.ID)
io.WriteString(c.w, ` FROM (`) io.WriteString(c.w, ` FROM (`)
} }
@ -245,8 +277,8 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
io.WriteString(c.w, `row_to_json((`) io.WriteString(c.w, `row_to_json((`)
//fmt.Fprintf(w, `SELECT "sel_%d" FROM (SELECT `, c.sel.ID) //fmt.Fprintf(w, `SELECT "%d" FROM (SELECT `, c.sel.ID)
io.WriteString(c.w, `SELECT "sel_`) io.WriteString(c.w, `SELECT "json_row_`)
int2string(c.w, sel.ID) int2string(c.w, sel.ID)
io.WriteString(c.w, `" FROM (SELECT `) io.WriteString(c.w, `" FROM (SELECT `)
@ -260,13 +292,13 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
return skipped, err return skipped, err
} }
//fmt.Fprintf(w, `) AS "sel_%d"`, c.sel.ID) //fmt.Fprintf(w, `) AS "%d"`, c.sel.ID)
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
aliasWithID(c.w, "sel", sel.ID) aliasWithID(c.w, "json_row", sel.ID)
//fmt.Fprintf(w, `)) AS "%s"`, c.sel.Table) //fmt.Fprintf(w, `)) AS "%s"`, c.sel.Table)
io.WriteString(c.w, `))`) io.WriteString(c.w, `))`)
aliasWithID(c.w, "sel_json", sel.ID) aliasWithID(c.w, "json", sel.ID)
// END-ROW-TO-JSON // END-ROW-TO-JSON
if hasOrder { if hasOrder {
@ -295,8 +327,8 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo)
} }
switch { switch {
case sel.Paging.NoLimit: case ti.Singular:
break io.WriteString(c.w, ` LIMIT ('1') :: integer`)
case len(sel.Paging.Limit) != 0: case len(sel.Paging.Limit) != 0:
//fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit) //fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit)
@ -304,8 +336,8 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo)
io.WriteString(c.w, sel.Paging.Limit) io.WriteString(c.w, sel.Paging.Limit)
io.WriteString(c.w, `') :: integer`) io.WriteString(c.w, `') :: integer`)
case ti.Singular: case sel.Paging.NoLimit:
io.WriteString(c.w, ` LIMIT ('1') :: integer`) break
default: default:
io.WriteString(c.w, ` LIMIT ('20') :: integer`) io.WriteString(c.w, ` LIMIT ('20') :: integer`)
@ -318,10 +350,10 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo)
io.WriteString(c.w, `') :: integer`) io.WriteString(c.w, `') :: integer`)
} }
if ti.Singular == false { if !ti.Singular {
//fmt.Fprintf(w, `) AS "sel_json_agg_%d"`, c.sel.ID) //fmt.Fprintf(w, `) AS "json_agg_%d"`, c.sel.ID)
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
aliasWithID(c.w, "sel_json_agg", sel.ID) aliasWithID(c.w, "json_agg", sel.ID)
} }
return nil return nil
@ -384,7 +416,7 @@ func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) {
for _, col := range sel.Cols { for _, col := range sel.Cols {
n := funcPrefixLen(col.Name) n := funcPrefixLen(col.Name)
if n != 0 { if n != 0 {
if sel.Functions == false { if !sel.Functions {
continue continue
} }
if len(sel.Allowed) != 0 { if len(sel.Allowed) != 0 {
@ -445,24 +477,18 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo
} }
childSel := &c.s[id] childSel := &c.s[id]
cti, err := c.schema.GetTable(childSel.Table)
if err != nil {
return err
}
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`, //fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
//s.Table, s.ID, s.Table, s.FieldName) //s.Table, s.ID, s.Table, s.FieldName)
if cti.Singular { //if cti.Singular {
io.WriteString(c.w, `"sel_json_`) io.WriteString(c.w, `"`)
int2string(c.w, childSel.ID) io.WriteString(c.w, childSel.Table)
io.WriteString(c.w, `" AS "`) io.WriteString(c.w, `_`)
io.WriteString(c.w, childSel.FieldName) int2string(c.w, childSel.ID)
io.WriteString(c.w, `"`) io.WriteString(c.w, `_join"."json_`)
int2string(c.w, childSel.ID)
} else { io.WriteString(c.w, `" AS "`)
colWithTableIDSuffixAlias(c.w, childSel.Table, childSel.ID, io.WriteString(c.w, childSel.FieldName)
"_join", childSel.Table, childSel.FieldName) io.WriteString(c.w, `"`)
}
} }
return nil return nil
@ -472,8 +498,8 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
childCols []*qcode.Column, skipped uint32) error { childCols []*qcode.Column, skipped uint32) error {
var groupBy []int var groupBy []int
isRoot := sel.ID == 0 isRoot := sel.ParentID == -1
isFil := sel.Where != nil isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop)
isSearch := sel.Args["search"] != nil isSearch := sel.Args["search"] != nil
isAgg := false isAgg := false
@ -641,8 +667,8 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
} }
switch { switch {
case sel.Paging.NoLimit: case ti.Singular:
break io.WriteString(c.w, ` LIMIT ('1') :: integer`)
case len(sel.Paging.Limit) != 0: case len(sel.Paging.Limit) != 0:
//fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit) //fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit)
@ -650,8 +676,8 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
io.WriteString(c.w, sel.Paging.Limit) io.WriteString(c.w, sel.Paging.Limit)
io.WriteString(c.w, `') :: integer`) io.WriteString(c.w, `') :: integer`)
case ti.Singular: case sel.Paging.NoLimit:
io.WriteString(c.w, ` LIMIT ('1') :: integer`) break
default: default:
io.WriteString(c.w, ` LIMIT ('20') :: integer`) io.WriteString(c.w, ` LIMIT ('20') :: integer`)
@ -817,7 +843,6 @@ func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error
} }
func (c *compilerContext) renderNestedWhere(ex *qcode.Exp, sel *qcode.Select, ti *DBTableInfo) error { func (c *compilerContext) renderNestedWhere(ex *qcode.Exp, sel *qcode.Select, ti *DBTableInfo) error {
for i := 0; i < len(ex.NestedCols)-1; i++ { for i := 0; i < len(ex.NestedCols)-1; i++ {
cti, err := c.schema.GetTable(ex.NestedCols[i]) cti, err := c.schema.GetTable(ex.NestedCols[i])
if err != nil { if err != nil {
@ -851,7 +876,18 @@ func (c *compilerContext) renderNestedWhere(ex *qcode.Exp, sel *qcode.Select, ti
} }
func (c *compilerContext) renderOp(ex *qcode.Exp, sel *qcode.Select, ti *DBTableInfo) error { func (c *compilerContext) renderOp(ex *qcode.Exp, sel *qcode.Select, ti *DBTableInfo) error {
var col *DBColumn
var ok bool
if ex.Op == qcode.OpNop {
return nil
}
if len(ex.Col) != 0 { if len(ex.Col) != 0 {
if col, ok = ti.Columns[ex.Col]; !ok {
return fmt.Errorf("no column '%s' found ", ex.Col)
}
io.WriteString(c.w, `((`) io.WriteString(c.w, `((`)
colWithTable(c.w, ti.Name, ex.Col) colWithTable(c.w, ti.Name, ex.Col)
io.WriteString(c.w, `) `) io.WriteString(c.w, `) `)
@ -907,6 +943,9 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, sel *qcode.Select, ti *DBTable
if len(ti.PrimaryCol) == 0 { if len(ti.PrimaryCol) == 0 {
return fmt.Errorf("no primary key column defined for %s", ti.Name) return fmt.Errorf("no primary key column defined for %s", ti.Name)
} }
if col, ok = ti.Columns[ti.PrimaryCol]; !ok {
return fmt.Errorf("no primary key column '%s' found ", ti.PrimaryCol)
}
//fmt.Fprintf(w, `(("%s") =`, c.ti.PrimaryCol) //fmt.Fprintf(w, `(("%s") =`, c.ti.PrimaryCol)
io.WriteString(c.w, `((`) io.WriteString(c.w, `((`)
colWithTable(c.w, ti.Name, ti.PrimaryCol) colWithTable(c.w, ti.Name, ti.PrimaryCol)
@ -916,6 +955,9 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, sel *qcode.Select, ti *DBTable
if len(ti.TSVCol) == 0 { if len(ti.TSVCol) == 0 {
return fmt.Errorf("no tsv column defined for %s", ti.Name) return fmt.Errorf("no tsv column defined for %s", ti.Name)
} }
if _, ok = ti.Columns[ti.TSVCol]; !ok {
return fmt.Errorf("no tsv column '%s' found ", ti.TSVCol)
}
//fmt.Fprintf(w, `(("%s") @@ to_tsquery('%s'))`, c.ti.TSVCol, val.Val) //fmt.Fprintf(w, `(("%s") @@ to_tsquery('%s'))`, c.ti.TSVCol, val.Val)
io.WriteString(c.w, `(("`) io.WriteString(c.w, `(("`)
io.WriteString(c.w, ti.TSVCol) io.WriteString(c.w, ti.TSVCol)
@ -931,7 +973,7 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, sel *qcode.Select, ti *DBTable
if ex.Type == qcode.ValList { if ex.Type == qcode.ValList {
c.renderList(ex) c.renderList(ex)
} else { } else {
c.renderVal(ex, c.vars) c.renderVal(ex, c.vars, col)
} }
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
@ -1008,7 +1050,7 @@ func (c *compilerContext) renderList(ex *qcode.Exp) {
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
} }
func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string) { func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *DBColumn) {
io.WriteString(c.w, ` `) io.WriteString(c.w, ` `)
switch ex.Type { switch ex.Type {
@ -1025,6 +1067,7 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string) {
io.WriteString(c.w, `'`) io.WriteString(c.w, `'`)
case qcode.ValVar: case qcode.ValVar:
io.WriteString(c.w, `'`)
if val, ok := vars[ex.Val]; ok { if val, ok := vars[ex.Val]; ok {
io.WriteString(c.w, val) io.WriteString(c.w, val)
} else { } else {
@ -1033,6 +1076,8 @@ func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string) {
io.WriteString(c.w, ex.Val) io.WriteString(c.w, ex.Val)
io.WriteString(c.w, `}}`) io.WriteString(c.w, `}}`)
} }
io.WriteString(c.w, `' :: `)
io.WriteString(c.w, col.Type)
} }
//io.WriteString(c.w, `)`) //io.WriteString(c.w, `)`)
} }
@ -1093,22 +1138,6 @@ func aliasWithIDSuffix(w io.Writer, alias string, id int32, suffix string) {
io.WriteString(w, `"`) io.WriteString(w, `"`)
} }
func colWithAlias(w io.Writer, col, alias string) {
io.WriteString(w, `"`)
io.WriteString(w, col)
io.WriteString(w, `" AS "`)
io.WriteString(w, alias)
io.WriteString(w, `"`)
}
func tableWithAlias(w io.Writer, table, alias string) {
io.WriteString(w, `"`)
io.WriteString(w, table)
io.WriteString(w, `" AS "`)
io.WriteString(w, alias)
io.WriteString(w, `"`)
}
func colWithTable(w io.Writer, table, col string) { func colWithTable(w io.Writer, table, col string) {
io.WriteString(w, `"`) io.WriteString(w, `"`)
io.WriteString(w, table) io.WriteString(w, table)
@ -1139,20 +1168,6 @@ func colWithTableIDAlias(w io.Writer, table string, id int32, col, alias string)
io.WriteString(w, `"`) io.WriteString(w, `"`)
} }
func colWithTableIDSuffixAlias(w io.Writer, table string, id int32,
suffix, col, alias string) {
io.WriteString(w, `"`)
io.WriteString(w, table)
io.WriteString(w, `_`)
int2string(w, id)
io.WriteString(w, suffix)
io.WriteString(w, `"."`)
io.WriteString(w, col)
io.WriteString(w, `" AS "`)
io.WriteString(w, alias)
io.WriteString(w, `"`)
}
func tableIDColSuffix(w io.Writer, table string, id int32, col, suffix string) { func tableIDColSuffix(w io.Writer, table string, id int32, col, suffix string) {
io.WriteString(w, `"`) io.WriteString(w, `"`)
io.WriteString(w, table) io.WriteString(w, table)
@ -1177,7 +1192,7 @@ func int2string(w io.Writer, val int32) {
for val2 > 0 { for val2 > 0 {
temp *= 10 temp *= 10
temp += val2 % 10 temp += val2 % 10
val2 = int32(math.Floor(float64(val2 / 10))) val2 = int32(float64(val2 / 10))
} }
val3 := temp val3 := temp
@ -1187,11 +1202,3 @@ func int2string(w io.Writer, val int32) {
w.Write([]byte{charset[d]}) w.Write([]byte{charset[d]})
} }
} }
func relID(h *xxhash.Digest, child, parent string) uint64 {
h.WriteString(child)
h.WriteString(parent)
v := h.Sum64()
h.Reset()
return v
}

View File

@ -28,7 +28,7 @@ func withComplexArgs(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0" ORDER BY "products_0_price_ob" DESC), '[]') AS "products" FROM (SELECT DISTINCT ON ("products_0_price_ob") row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` 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"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -56,7 +56,7 @@ func withWhereMultiOr(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -82,7 +82,7 @@ func withWhereIsNull(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -108,7 +108,7 @@ func withWhereAndList(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -128,7 +128,7 @@ func fetchByID(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_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 "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -148,7 +148,7 @@ func searchQuery(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("tsv") @@ to_tsquery('Imperial'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("tsv") @@ to_tsquery('Imperial'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -171,7 +171,7 @@ func oneToMany(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."products" AS "products") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("sel_json_1"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "sel_1")) AS "sel_json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "sel_json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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"))) 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -194,7 +194,7 @@ func belongsTo(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "users_1_join"."users" AS "users") AS "sel_0")) AS "sel_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("sel_json_1"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "users_1"."email" AS "email") AS "sel_1")) AS "sel_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 "sel_json_agg_1") AS "users_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -217,7 +217,7 @@ func manyToMany(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "customers_1_join"."customers" AS "customers") AS "sel_0")) AS "sel_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("sel_json_1"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "sel_1")) AS "sel_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 "sel_json_agg_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -240,7 +240,7 @@ func manyToManyReverse(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('customers', customers) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."products" AS "products") AS "sel_0")) AS "sel_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("sel_json_1"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name") AS "sel_1")) AS "sel_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"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "sel_json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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"))) 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -260,7 +260,7 @@ func aggFunction(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -280,7 +280,7 @@ func aggFunctionBlockedByCol(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "anon")
if err != nil { if err != nil {
@ -300,7 +300,7 @@ func aggFunctionDisabled(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "anon1")
if err != nil { if err != nil {
@ -320,7 +320,7 @@ func aggFunctionWithFilter(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."max_price" AS "max_price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -339,7 +339,7 @@ func syntheticTables(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('me', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT ) AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = {{user_id}})) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "done_1337"` 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") = '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -359,7 +359,7 @@ func queryWithVariables(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."price") = {{product_price}}) AND (("products"."id") = {{product_id}})) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "done_1337"` 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") = '{{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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -385,7 +385,40 @@ func withWhereOnRelations(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")))) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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")))) 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)
}
}
func multiRoot(t *testing.T) {
gql := `query {
product {
id
name
customer {
email
}
customers {
email
}
}
user {
id
email
}
customer {
id
}
}`
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"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -406,7 +439,7 @@ func blockedQuery(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email") AS "sel_0")) AS "sel_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 "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
if err != nil { if err != nil {
@ -426,7 +459,7 @@ func blockedFunctions(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."email" FROM "users" WHERE (false) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` 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") resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
if err != nil { if err != nil {
@ -456,6 +489,7 @@ func TestCompileQuery(t *testing.T) {
t.Run("syntheticTables", syntheticTables) t.Run("syntheticTables", syntheticTables)
t.Run("queryWithVariables", queryWithVariables) t.Run("queryWithVariables", queryWithVariables)
t.Run("withWhereOnRelations", withWhereOnRelations) t.Run("withWhereOnRelations", withWhereOnRelations)
t.Run("multiRoot", multiRoot)
t.Run("blockedQuery", blockedQuery) t.Run("blockedQuery", blockedQuery)
t.Run("blockedFunctions", blockedFunctions) t.Run("blockedFunctions", blockedFunctions)
} }

View File

@ -199,7 +199,9 @@ func NewDBSchema(db *pgxpool.Pool, aliases map[string][]string) (*DBSchema, erro
return nil, err return nil, err
} }
schema.updateSchema(t, cols, aliases) if err := schema.updateSchema(t, cols, aliases); err != nil {
return nil, err
}
} }
return schema, nil return schema, nil
@ -208,7 +210,7 @@ func NewDBSchema(db *pgxpool.Pool, aliases map[string][]string) (*DBSchema, erro
func (s *DBSchema) updateSchema( func (s *DBSchema) updateSchema(
t *DBTable, t *DBTable,
cols []*DBColumn, cols []*DBColumn,
aliases map[string][]string) { aliases map[string][]string) error {
// Foreign key columns in current table // Foreign key columns in current table
colByID := make(map[int16]*DBColumn) colByID := make(map[int16]*DBColumn)
@ -281,12 +283,16 @@ func (s *DBSchema) updateSchema(
// Belongs-to relation between current table and the // Belongs-to relation between current table and the
// table in the foreign key // table in the foreign key
rel1 := &DBRel{RelBelongTo, "", "", c.Name, fc.Name} rel1 := &DBRel{RelBelongTo, "", "", c.Name, fc.Name}
s.SetRel(ct, ft, rel1) if err := s.SetRel(ct, ft, rel1); err != nil {
return err
}
// One-to-many relation between the foreign key table and the // One-to-many relation between the foreign key table and the
// the current table // the current table
rel2 := &DBRel{RelOneToMany, "", "", fc.Name, c.Name} rel2 := &DBRel{RelOneToMany, "", "", fc.Name, c.Name}
s.SetRel(ft, ct, rel2) if err := s.SetRel(ft, ct, rel2); err != nil {
return err
}
jcols = append(jcols, c) jcols = append(jcols, c)
} }
@ -301,42 +307,54 @@ func (s *DBSchema) updateSchema(
if len(jcols) > 1 { if len(jcols) > 1 {
for i := range jcols { for i := range jcols {
for n := range jcols { for n := range jcols {
if n != i { if n == i {
s.updateSchemaOTMT(ct, jcols[i], jcols[n], colByID) continue
}
err := s.updateSchemaOTMT(ct, jcols[i], jcols[n], colByID)
if err != nil {
return err
} }
} }
} }
} }
return nil
} }
func (s *DBSchema) updateSchemaOTMT( func (s *DBSchema) updateSchemaOTMT(
ct string, ct string,
col1, col2 *DBColumn, col1, col2 *DBColumn,
colByID map[int16]*DBColumn) { colByID map[int16]*DBColumn) error {
t1 := strings.ToLower(col1.FKeyTable) t1 := strings.ToLower(col1.FKeyTable)
t2 := strings.ToLower(col2.FKeyTable) t2 := strings.ToLower(col2.FKeyTable)
fc1, ok := colByID[col1.FKeyColID[0]] fc1, ok := colByID[col1.FKeyColID[0]]
if !ok { if !ok {
return return fmt.Errorf("expected column id '%d' not found", col1.FKeyColID[0])
} }
fc2, ok := colByID[col2.FKeyColID[0]] fc2, ok := colByID[col2.FKeyColID[0]]
if !ok { if !ok {
return return fmt.Errorf("expected column id '%d' not found", col2.FKeyColID[0])
} }
// One-to-many-through relation between 1nd foreign key table and the // One-to-many-through relation between 1nd foreign key table and the
// 2nd foreign key table // 2nd foreign key table
//rel1 := &DBRel{RelOneToManyThrough, ct, fc1.Name, col1.Name} //rel1 := &DBRel{RelOneToManyThrough, ct, fc1.Name, col1.Name}
rel1 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name, col1.Name} rel1 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name, col1.Name}
s.SetRel(t1, t2, rel1) if err := s.SetRel(t1, t2, rel1); err != nil {
return err
}
// One-to-many-through relation between 2nd foreign key table and the // One-to-many-through relation between 2nd foreign key table and the
// 1nd foreign key table // 1nd foreign key table
//rel2 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name} //rel2 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name}
rel2 := &DBRel{RelOneToManyThrough, ct, col1.Name, fc1.Name, col2.Name} rel2 := &DBRel{RelOneToManyThrough, ct, col1.Name, fc1.Name, col2.Name}
s.SetRel(t2, t1, rel2) if err := s.SetRel(t2, t1, rel2); err != nil {
return err
}
return nil
} }
func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) { func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {

View File

@ -1,6 +1,7 @@
package qcode package qcode
import ( import (
"regexp"
"sort" "sort"
"strings" "strings"
) )
@ -115,9 +116,18 @@ func listToMap(list []string) map[string]struct{} {
func mapToList(m map[string]string) []string { func mapToList(m map[string]string) []string {
list := []string{} list := []string{}
for k, _ := range m { for k := range m {
list = append(list, strings.ToLower(k)) list = append(list, strings.ToLower(k))
} }
sort.Strings(list) sort.Strings(list)
return list return list
} }
var varRe = regexp.MustCompile(`\$([a-zA-Z0-9_]+)`)
func parsePresets(m map[string]string) map[string]string {
for k, v := range m {
m[k] = varRe.ReplaceAllString(v, `{{$1}}`)
}
return m
}

View File

@ -4,6 +4,8 @@ package qcode
// FuzzerEntrypoint for Fuzzbuzz // FuzzerEntrypoint for Fuzzbuzz
func Fuzz(data []byte) int { func Fuzz(data []byte) int {
GetQType(string(data))
qcompile, _ := NewCompiler(Config{}) qcompile, _ := NewCompiler(Config{})
_, err := qcompile.Compile(data, "user") _, err := qcompile.Compile(data, "user")
if err != nil { if err != nil {

View File

@ -461,7 +461,7 @@ func (i *item) String() string {
case itemStringVal: case itemStringVal:
v = "string" v = "string"
} }
return fmt.Sprintf("%s", v) return v
} }
/* /*

View File

@ -85,7 +85,6 @@ type Parser struct {
input []byte // the string being scanned input []byte // the string being scanned
pos int pos int
items []item items []item
depth int
err error err error
} }
@ -148,24 +147,13 @@ func parseSelectionSet(gql []byte) (*Operation, error) {
if p.peek(itemObjOpen) { if p.peek(itemObjOpen) {
p.ignore() p.ignore()
} op, err = p.parseQueryOp()
if p.peek(itemName) {
op = opPool.Get().(*Operation)
op.Reset()
op.Type = opQuery
op.Name = ""
op.Fields = op.fieldsA[:0]
op.Args = op.argsA[:0]
op.Fields, err = p.parseFields(op.Fields)
} else { } else {
op, err = p.parseOp() op, err = p.parseOp()
}
if err != nil { if err != nil {
return nil, err return nil, err
}
} }
lexPool.Put(l) lexPool.Put(l)
@ -196,15 +184,6 @@ func (p *Parser) ignore() {
p.pos = n p.pos = n
} }
func (p *Parser) current() item {
return p.items[p.pos]
}
func (p *Parser) eof() bool {
n := p.pos + 1
return p.items[n].typ == itemEOF
}
func (p *Parser) peek(types ...itemType) bool { func (p *Parser) peek(types ...itemType) bool {
n := p.pos + 1 n := p.pos + 1
if p.items[n].typ == itemEOF { if p.items[n].typ == itemEOF {
@ -259,6 +238,37 @@ func (p *Parser) parseOp() (*Operation, error) {
if p.peek(itemObjOpen) { if p.peek(itemObjOpen) {
p.ignore() p.ignore()
for n := 0; n < 10; n++ {
if !p.peek(itemName) {
break
}
op.Fields, err = p.parseFields(op.Fields)
if err != nil {
return nil, err
}
}
}
return op, nil
}
func (p *Parser) parseQueryOp() (*Operation, error) {
op := opPool.Get().(*Operation)
op.Reset()
op.Type = opQuery
op.Fields = op.fieldsA[:0]
op.Args = op.argsA[:0]
var err error
for n := 0; n < 10; n++ {
if !p.peek(itemName) {
break
}
op.Fields, err = p.parseFields(op.Fields) op.Fields, err = p.parseFields(op.Fields)
if err != nil { if err != nil {
return nil, err return nil, err
@ -286,7 +296,7 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
continue continue
} }
if p.peek(itemName) == false { if !p.peek(itemName) {
return nil, errors.New("expecting an alias or field name") return nil, errors.New("expecting an alias or field name")
} }
@ -300,16 +310,12 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
return nil, err return nil, err
} }
if f.ID != 0 { intf := st.Peek()
intf := st.Peek() if pid, ok := intf.(int32); ok {
pid, ok := intf.(int32)
if !ok {
return nil, fmt.Errorf("14: unexpected value %v (%t)", intf, intf)
}
f.ParentID = pid f.ParentID = pid
fields[pid].Children = append(fields[pid].Children, f.ID) fields[pid].Children = append(fields[pid].Children, f.ID)
} else {
f.ParentID = -1
} }
if p.peek(itemObjOpen) { if p.peek(itemObjOpen) {
@ -358,13 +364,13 @@ func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
p.ignore() p.ignore()
break break
} }
if p.peek(itemName) == false { if !p.peek(itemName) {
return nil, errors.New("expecting an argument name") return nil, errors.New("expecting an argument name")
} }
args = append(args, Arg{Name: p.val(p.next())}) args = append(args, Arg{Name: p.val(p.next())})
arg := &args[(len(args) - 1)] arg := &args[(len(args) - 1)]
if p.peek(itemColon) == false { if !p.peek(itemColon) {
return nil, errors.New("missing ':' after argument name") return nil, errors.New("missing ':' after argument name")
} }
p.ignore() p.ignore()
@ -425,12 +431,12 @@ func (p *Parser) parseObj() (*Node, error) {
break break
} }
if p.peek(itemName) == false { if !p.peek(itemName) {
return nil, errors.New("expecting an argument name") return nil, errors.New("expecting an argument name")
} }
nodeName := p.val(p.next()) nodeName := p.val(p.next())
if p.peek(itemColon) == false { if !p.peek(itemColon) {
return nil, errors.New("missing ':' after Field argument name") return nil, errors.New("missing ':' after Field argument name")
} }
p.ignore() p.ignore()

View File

@ -7,17 +7,20 @@ import (
func TestCompile1(t *testing.T) { func TestCompile1(t *testing.T) {
qc, _ := NewCompiler(Config{}) qc, _ := NewCompiler(Config{})
qc.AddRole("user", "product", TRConfig{ err := qc.AddRole("user", "product", TRConfig{
Query: QueryConfig{ Query: QueryConfig{
Columns: []string{"id", "Name"}, Columns: []string{"id", "Name"},
}, },
}) })
if err != nil {
t.Error(err)
}
_, err := qc.Compile([]byte(` _, err = qc.Compile([]byte(`
product(id: 15) { { product(id: 15) {
id id
name name
}`), "user") } }`), "user")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -26,13 +29,16 @@ func TestCompile1(t *testing.T) {
func TestCompile2(t *testing.T) { func TestCompile2(t *testing.T) {
qc, _ := NewCompiler(Config{}) qc, _ := NewCompiler(Config{})
qc.AddRole("user", "product", TRConfig{ err := qc.AddRole("user", "product", TRConfig{
Query: QueryConfig{ Query: QueryConfig{
Columns: []string{"ID"}, Columns: []string{"ID"},
}, },
}) })
if err != nil {
t.Error(err)
}
_, err := qc.Compile([]byte(` _, err = qc.Compile([]byte(`
query { product(id: 15) { query { product(id: 15) {
id id
name name
@ -45,13 +51,16 @@ func TestCompile2(t *testing.T) {
func TestCompile3(t *testing.T) { func TestCompile3(t *testing.T) {
qc, _ := NewCompiler(Config{}) qc, _ := NewCompiler(Config{})
qc.AddRole("user", "product", TRConfig{ err := qc.AddRole("user", "product", TRConfig{
Query: QueryConfig{ Query: QueryConfig{
Columns: []string{"ID"}, Columns: []string{"ID"},
}, },
}) })
if err != nil {
t.Error(err)
}
_, err := qc.Compile([]byte(` _, err = qc.Compile([]byte(`
mutation { mutation {
product(id: 15, name: "Test") { product(id: 15, name: "Test") {
id id

View File

@ -20,6 +20,7 @@ const (
const ( const (
QTQuery QType = iota + 1 QTQuery QType = iota + 1
QTMutation
QTInsert QTInsert
QTUpdate QTUpdate
QTDelete QTDelete
@ -30,6 +31,8 @@ type QCode struct {
Type QType Type QType
ActionVar string ActionVar string
Selects []Select Selects []Select
Roots []int32
rootsA [5]int32
} }
type Select struct { type Select struct {
@ -200,7 +203,7 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
return err return err
} }
trv.insert.cols = listToMap(trc.Insert.Columns) trv.insert.cols = listToMap(trc.Insert.Columns)
trv.insert.psmap = trc.Insert.Presets trv.insert.psmap = parsePresets(trc.Insert.Presets)
trv.insert.pslist = mapToList(trv.insert.psmap) trv.insert.pslist = mapToList(trv.insert.psmap)
// update config // update config
@ -208,7 +211,7 @@ func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
return err return err
} }
trv.update.cols = listToMap(trc.Update.Columns) trv.update.cols = listToMap(trc.Update.Columns)
trv.update.psmap = trc.Update.Presets trv.update.psmap = parsePresets(trc.Update.Presets)
trv.update.pslist = mapToList(trv.update.psmap) trv.update.pslist = mapToList(trv.update.psmap)
// delete config // delete config
@ -233,6 +236,7 @@ func (com *Compiler) Compile(query []byte, role string) (*QCode, error) {
var err error var err error
qc := QCode{Type: QTQuery} qc := QCode{Type: QTQuery}
qc.Roots = qc.rootsA[:0]
op, err := Parse(query) op, err := Parse(query)
if err != nil { if err != nil {
@ -250,7 +254,7 @@ func (com *Compiler) Compile(query []byte, role string) (*QCode, error) {
func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
id := int32(0) id := int32(0)
parentID := int32(0) parentID := int32(-1)
if len(op.Fields) == 0 { if len(op.Fields) == 0 {
return errors.New("invalid graphql no query found") return errors.New("invalid graphql no query found")
@ -269,7 +273,12 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
if len(op.Fields) == 0 { if len(op.Fields) == 0 {
return errors.New("empty query") return errors.New("empty query")
} }
st.Push(op.Fields[0].ID)
for i := range op.Fields {
if op.Fields[i].ParentID == -1 {
st.Push(op.Fields[i].ID)
}
}
for { for {
if st.Len() == 0 { if st.Len() == 0 {
@ -313,11 +322,6 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
s.PresetList = trv.update.pslist s.PresetList = trv.update.pslist
} }
if s.ID != 0 {
p := &selects[s.ParentID]
p.Children = append(p.Children, s.ID)
}
if len(field.Alias) != 0 { if len(field.Alias) != 0 {
s.FieldName = field.Alias s.FieldName = field.Alias
} else { } else {
@ -329,6 +333,15 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
return err return err
} }
// Order is important addFilters must come after compileArgs
if s.ParentID == -1 {
qc.Roots = append(qc.Roots, s.ID)
com.addFilters(qc, s, role)
} else {
p := &selects[s.ParentID]
p.Children = append(p.Children, s.ID)
}
s.Cols = make([]Column, 0, len(field.Children)) s.Cols = make([]Column, 0, len(field.Children))
action = QTQuery action = QTQuery
@ -362,36 +375,40 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
return errors.New("invalid query") return errors.New("invalid query")
} }
var fil *Exp qc.Selects = selects[:id]
root := &selects[0] return nil
}
if trv, ok := com.tr[role][op.Fields[0].Name]; ok { func (com *Compiler) addFilters(qc *QCode, root *Select, role string) {
var fil *Exp
if trv, ok := com.tr[role][root.Table]; ok {
fil = trv.filter(qc.Type) fil = trv.filter(qc.Type)
} }
if fil != nil { if fil == nil {
switch fil.Op { return
case OpNop:
case OpFalse:
root.Where = fil
default:
if root.Where != nil {
ow := root.Where
root.Where = expPool.Get().(*Exp)
root.Where.Reset()
root.Where.Op = OpAnd
root.Where.Children = root.Where.childrenA[:2]
root.Where.Children[0] = fil
root.Where.Children[1] = ow
} else {
root.Where = fil
}
}
} }
qc.Selects = selects[:id] switch fil.Op {
return nil case OpNop:
case OpFalse:
root.Where = fil
default:
if root.Where != nil {
ow := root.Where
root.Where = expPool.Get().(*Exp)
root.Where.Reset()
root.Where.Op = OpAnd
root.Where.Children = root.Where.childrenA[:2]
root.Where.Children[0] = fil
root.Where.Children[1] = ow
} else {
root.Where = fil
}
}
} }
func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg) error { func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg) error {

23
qcode/utils.go Normal file
View File

@ -0,0 +1,23 @@
package qcode
func GetQType(gql string) QType {
for i := range gql {
b := gql[i]
if b == '{' {
return QTQuery
}
if al(b) {
switch b {
case 'm', 'M':
return QTMutation
case 'q', 'Q':
return QTQuery
}
}
}
return -1
}
func al(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
}

View File

@ -18,6 +18,8 @@ const (
) )
type allowItem struct { type allowItem struct {
name string
hash string
uri string uri string
gql string gql string
vars json.RawMessage vars json.RawMessage
@ -46,7 +48,7 @@ func initAllowList(cpath string) {
if _, err := os.Stat(fp); err == nil { if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp _allowList.filepath = fp
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
@ -56,7 +58,7 @@ func initAllowList(cpath string) {
if _, err := os.Stat(fp); err == nil { if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp _allowList.filepath = fp
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
@ -66,13 +68,13 @@ func initAllowList(cpath string) {
if _, err := os.Stat(fp); err == nil { if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp _allowList.filepath = fp
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
if len(_allowList.filepath) == 0 { if len(_allowList.filepath) == 0 {
if conf.Production { if conf.Production {
logger.Fatal().Msg("allow.list not found") errlog.Fatal().Msg("allow.list not found")
} }
if len(cpath) == 0 { if len(cpath) == 0 {
@ -94,7 +96,7 @@ func initAllowList(cpath string) {
} }
func (al *allowList) add(req *gqlReq) { func (al *allowList) add(req *gqlReq) {
if al.active == false || len(req.ref) == 0 || len(req.Query) == 0 { if len(req.ref) == 0 || len(req.Query) == 0 {
return return
} }
@ -119,11 +121,39 @@ func (al *allowList) add(req *gqlReq) {
} }
} }
func (al *allowList) load() { func (al *allowList) upsert(query, vars []byte, uri string) {
if al.active == false { q := string(query)
return hash := gqlHash(q, vars, "")
name := gqlName(q)
var key string
if len(name) == 0 {
key = hash
} else {
key = name
} }
if i, ok := al.index[key]; !ok {
al.list = append(al.list, &allowItem{
name: name,
hash: hash,
uri: uri,
gql: q,
vars: vars,
})
al.index[key] = len(al.list) - 1
} else {
item := al.list[i]
item.name = name
item.hash = hash
item.gql = q
item.vars = vars
}
}
func (al *allowList) load() {
b, err := ioutil.ReadFile(al.filepath) b, err := ioutil.ReadFile(al.filepath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -172,22 +202,7 @@ func (al *allowList) load() {
if c == 0 { if c == 0 {
if ty == AL_QUERY { if ty == AL_QUERY {
q := string(b[s:(e + 1)]) al.upsert(b[s:(e+1)], varBytes, uri)
key := gqlHash(q, varBytes, "")
if idx, ok := al.index[key]; !ok {
al.list = append(al.list, &allowItem{
uri: uri,
gql: q,
vars: varBytes,
})
al.index[key] = len(al.list) - 1
} else {
item := al.list[idx]
item.gql = q
item.vars = varBytes
}
varBytes = nil varBytes = nil
} else if ty == AL_VARS { } else if ty == AL_VARS {
@ -205,19 +220,35 @@ func (al *allowList) load() {
} }
func (al *allowList) save(item *allowItem) { func (al *allowList) save(item *allowItem) {
if al.active == false { var err error
return
item.hash = gqlHash(item.gql, item.vars, "")
item.name = gqlName(item.gql)
if len(item.name) == 0 {
key := item.hash
if _, ok := al.index[key]; ok {
return
}
al.list = append(al.list, item)
al.index[key] = len(al.list) - 1
} else {
key := item.name
if i, ok := al.index[key]; ok {
if al.list[i].hash == item.hash {
return
}
al.list[i] = item
} else {
al.list = append(al.list, item)
al.index[key] = len(al.list) - 1
}
} }
key := gqlHash(item.gql, item.vars, "")
if _, ok := al.index[key]; ok {
return
}
al.list = append(al.list, item)
al.index[key] = len(al.list) - 1
f, err := os.Create(al.filepath) f, err := os.Create(al.filepath)
if err != nil { if err != nil {
logger.Warn().Err(err).Msgf("Failed to write allow list: %s", al.filepath) logger.Warn().Err(err).Msgf("Failed to write allow list: %s", al.filepath)
@ -242,22 +273,35 @@ func (al *allowList) save(item *allowItem) {
k := keys[i] k := keys[i]
v := urlMap[k] v := urlMap[k]
f.WriteString(fmt.Sprintf("# %s\n\n", k)) if _, err := f.WriteString(fmt.Sprintf("# %s\n\n", k)); err != nil {
logger.Error().Err(err).Send()
return
}
for i := range v { for i := range v {
if len(v[i].vars) != 0 && bytes.Equal(v[i].vars, []byte("{}")) == false { if len(v[i].vars) != 0 && !bytes.Equal(v[i].vars, []byte("{}")) {
vj, err := json.MarshalIndent(v[i].vars, "", "\t") vj, err := json.MarshalIndent(v[i].vars, "", "\t")
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("Failed to write allow list 'vars' to file") logger.Warn().Err(err).Msg("Failed to write allow list 'vars' to file")
continue continue
} }
f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
_, err = f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
if err != nil {
logger.Error().Err(err).Send()
return
}
} }
if v[i].gql[0] == '{' { if v[i].gql[0] == '{' {
f.WriteString(fmt.Sprintf("query %s\n\n", v[i].gql)) _, err = f.WriteString(fmt.Sprintf("query %s\n\n", v[i].gql))
} else { } else {
f.WriteString(fmt.Sprintf("%s\n\n", v[i].gql)) _, err = f.WriteString(fmt.Sprintf("%s\n\n", v[i].gql))
}
if err != nil {
logger.Error().Err(err).Send()
return
} }
} }
} }

View File

@ -2,63 +2,46 @@ package serv
import ( import (
"bytes" "bytes"
"context"
"errors"
"fmt" "fmt"
"io" "io"
"github.com/dosco/super-graph/jsn" "github.com/dosco/super-graph/jsn"
) )
func argMap(ctx *coreContext) func(w io.Writer, tag string) (int, error) { func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int, error) {
return func(w io.Writer, tag string) (int, error) { return func(w io.Writer, tag string) (int, error) {
switch tag { switch tag {
case "user_id_provider": case "user_id_provider":
if v := ctx.Value(userIDProviderKey); v != nil { if v := ctx.Value(userIDProviderKey); v != nil {
return stringArg(w, v.(string)) return io.WriteString(w, v.(string))
} }
io.WriteString(w, "null") return 0, errors.New("query requires variable $user_id_provider")
return 0, nil
case "user_id": case "user_id":
if v := ctx.Value(userIDKey); v != nil { if v := ctx.Value(userIDKey); v != nil {
return stringArg(w, v.(string)) return io.WriteString(w, v.(string))
} }
return 0, errors.New("query requires variable $user_id")
io.WriteString(w, "null")
return 0, nil
case "user_role": case "user_role":
if v := ctx.Value(userRoleKey); v != nil { if v := ctx.Value(userRoleKey); v != nil {
return stringArg(w, v.(string)) return io.WriteString(w, v.(string))
} }
io.WriteString(w, "null") return 0, errors.New("query requires variable $user_role")
}
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
if len(fields) == 0 {
return 0, nil return 0, nil
} }
fields := jsn.Get(ctx.req.Vars, [][]byte{[]byte(tag)}) return w.Write(fields[0].Value)
if len(fields) == 0 {
return 0, fmt.Errorf("variable '%s' not found", tag)
}
is := false
for i := range fields[0].Value {
c := fields[0].Value[i]
if c != ' ' {
is = (c == '"') || (c == '{') || (c == '[')
break
}
}
if is {
return stringArgB(w, fields[0].Value)
}
w.Write(fields[0].Value)
return 0, nil
} }
} }
func argList(ctx *coreContext, args [][]byte) []interface{} { func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
vars := make([]interface{}, len(args)) vars := make([]interface{}, len(args))
var fields map[string]interface{} var fields map[string]interface{}
@ -68,7 +51,7 @@ func argList(ctx *coreContext, args [][]byte) []interface{} {
fields, _, err = jsn.Tree(ctx.req.Vars) fields, _, err = jsn.Tree(ctx.req.Vars)
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("Failed to parse variables") return nil, err
} }
} }
@ -79,44 +62,33 @@ func argList(ctx *coreContext, args [][]byte) []interface{} {
case bytes.Equal(av, []byte("user_id")): case bytes.Equal(av, []byte("user_id")):
if v := ctx.Value(userIDKey); v != nil { if v := ctx.Value(userIDKey); v != nil {
vars[i] = v.(string) vars[i] = v.(string)
} else {
return nil, errors.New("query requires variable $user_id")
} }
case bytes.Equal(av, []byte("user_id_provider")): case bytes.Equal(av, []byte("user_id_provider")):
if v := ctx.Value(userIDProviderKey); v != nil { if v := ctx.Value(userIDProviderKey); v != nil {
vars[i] = v.(string) vars[i] = v.(string)
} else {
return nil, errors.New("query requires variable $user_id_provider")
} }
case bytes.Equal(av, []byte("user_role")): case bytes.Equal(av, []byte("user_role")):
if v := ctx.Value(userRoleKey); v != nil { if v := ctx.Value(userRoleKey); v != nil {
vars[i] = v.(string) vars[i] = v.(string)
} else {
return nil, errors.New("query requires variable $user_role")
} }
default: default:
if v, ok := fields[string(av)]; ok { if v, ok := fields[string(av)]; ok {
vars[i] = v vars[i] = v
} else {
return nil, fmt.Errorf("query requires variable $%s", string(av))
} }
} }
} }
return vars return vars, nil
}
func stringArg(w io.Writer, v string) (int, error) {
if n, err := w.Write([]byte(`'`)); err != nil {
return n, err
}
if n, err := w.Write([]byte(v)); err != nil {
return n, err
}
return w.Write([]byte(`'`))
}
func stringArgB(w io.Writer, v []byte) (int, error) {
if n, err := w.Write([]byte(`'`)); err != nil {
return n, err
}
if n, err := w.Write(v); err != nil {
return n, err
}
return w.Write([]byte(`'`))
} }

View File

@ -11,8 +11,7 @@ import (
const ( const (
authHeader = "Authorization" authHeader = "Authorization"
jwtBase int = iota jwtAuth0 int = iota + 1
jwtAuth0
) )
func jwtHandler(next http.HandlerFunc) http.HandlerFunc { func jwtHandler(next http.HandlerFunc) http.HandlerFunc {
@ -35,7 +34,7 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc {
case len(publicKeyFile) != 0: case len(publicKeyFile) != 0:
kd, err := ioutil.ReadFile(publicKeyFile) kd, err := ioutil.ReadFile(publicKeyFile)
if err != nil { if err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
switch conf.Auth.JWT.PubKeyType { switch conf.Auth.JWT.PubKeyType {
@ -51,7 +50,7 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc {
} }
if err != nil { if err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
@ -95,8 +94,11 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc {
} else { } else {
ctx = context.WithValue(ctx, userIDKey, claims.Subject) ctx = context.WithValue(ctx, userIDKey, claims.Subject)
} }
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
} }
} }

View File

@ -15,11 +15,11 @@ import (
func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc { func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
cookie := conf.Auth.Cookie cookie := conf.Auth.Cookie
if len(cookie) == 0 { if len(cookie) == 0 {
logger.Fatal().Msg("no auth.cookie defined") errlog.Fatal().Msg("no auth.cookie defined")
} }
if len(conf.Auth.Rails.URL) == 0 { if len(conf.Auth.Rails.URL) == 0 {
logger.Fatal().Msg("no auth.rails.url defined") errlog.Fatal().Msg("no auth.rails.url defined")
} }
rp := &redis.Pool{ rp := &redis.Pool{
@ -28,13 +28,13 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
Dial: func() (redis.Conn, error) { Dial: func() (redis.Conn, error) {
c, err := redis.DialURL(conf.Auth.Rails.URL) c, err := redis.DialURL(conf.Auth.Rails.URL)
if err != nil { if err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
pwd := conf.Auth.Rails.Password pwd := conf.Auth.Rails.Password
if len(pwd) != 0 { if len(pwd) != 0 {
if _, err := c.Do("AUTH", pwd); err != nil { if _, err := c.Do("AUTH", pwd); err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
return c, err return c, err
@ -69,16 +69,16 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc { func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc {
cookie := conf.Auth.Cookie cookie := conf.Auth.Cookie
if len(cookie) == 0 { if len(cookie) == 0 {
logger.Fatal().Msg("no auth.cookie defined") errlog.Fatal().Msg("no auth.cookie defined")
} }
if len(conf.Auth.Rails.URL) == 0 { if len(conf.Auth.Rails.URL) == 0 {
logger.Fatal().Msg("no auth.rails.url defined") errlog.Fatal().Msg("no auth.rails.url defined")
} }
rURL, err := url.Parse(conf.Auth.Rails.URL) rURL, err := url.Parse(conf.Auth.Rails.URL)
if err != nil { if err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
mc := memcache.New(rURL.Host) mc := memcache.New(rURL.Host)
@ -111,12 +111,12 @@ func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc {
func railsCookieHandler(next http.HandlerFunc) http.HandlerFunc { func railsCookieHandler(next http.HandlerFunc) http.HandlerFunc {
cookie := conf.Auth.Cookie cookie := conf.Auth.Cookie
if len(cookie) == 0 { if len(cookie) == 0 {
logger.Fatal().Msg("no auth.cookie defined") errlog.Fatal().Msg("no auth.cookie defined")
} }
ra, err := railsAuth(conf) ra, err := railsAuth(conf)
if err != nil { if err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
"github.com/dosco/super-graph/psql" "github.com/dosco/super-graph/psql"
@ -22,16 +23,26 @@ const (
) )
var ( var (
logger *zerolog.Logger // These variables are set using -ldflags
version string
gitBranch string
lastCommitSHA string
lastCommitTime string
)
var (
logger zerolog.Logger
errlog zerolog.Logger
conf *config conf *config
confPath string confPath string
db *pgxpool.Pool db *pgxpool.Pool
schema *psql.DBSchema
qcompile *qcode.Compiler qcompile *qcode.Compiler
pcompile *psql.Compiler pcompile *psql.Compiler
) )
func Init() { func Init() {
logger = initLog() initLog()
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: "super-graph", Use: "super-graph",
@ -110,6 +121,13 @@ e.g. db:migrate -+1
Run: cmdDBSetup, Run: cmdDBSetup,
}) })
rootCmd.AddCommand(&cobra.Command{
Use: "db:reset",
Short: "Reset database",
Long: "This command will drop, create, migrate and seed the database (won't run in production)",
Run: cmdDBReset,
})
rootCmd.AddCommand(&cobra.Command{ rootCmd.AddCommand(&cobra.Command{
Use: "new APP-NAME", Use: "new APP-NAME",
Short: "Create a new application", Short: "Create a new application",
@ -124,23 +142,24 @@ e.g. db:migrate -+1
Run: cmdConfDump, Run: cmdConfDump,
}) })
rootCmd.AddCommand(&cobra.Command{
Use: "version",
Short: "Super Graph binary version information",
Run: cmdVersion,
})
rootCmd.Flags().StringVar(&confPath, rootCmd.Flags().StringVar(&confPath,
"path", "./config", "path to config files") "path", "./config", "path to config files")
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
func initLog() *zerolog.Logger { func initLog() {
out := zerolog.ConsoleWriter{Out: os.Stderr} out := zerolog.ConsoleWriter{Out: os.Stderr}
logger := zerolog.New(out). logger = zerolog.New(out).With().Timestamp().Logger()
With(). errlog = logger.With().Caller().Logger()
Timestamp().
Caller().
Logger()
return &logger
} }
func initConf() (*config, error) { func initConf() (*config, error) {
@ -159,13 +178,16 @@ func initConf() (*config, error) {
} }
if vi.IsSet("inherits") { if vi.IsSet("inherits") {
logger.Fatal().Msgf("inherited config (%s) cannot itself inherit (%s)", errlog.Fatal().Msgf("inherited config (%s) cannot itself inherit (%s)",
inherits, inherits,
vi.GetString("inherits")) vi.GetString("inherits"))
} }
vi.SetConfigName(getConfigName()) vi.SetConfigName(getConfigName())
vi.MergeInConfig()
if err := vi.MergeInConfig(); err != nil {
return nil, err
}
} }
c := &config{} c := &config{}
@ -176,7 +198,7 @@ func initConf() (*config, error) {
logLevel, err := zerolog.ParseLevel(c.LogLevel) logLevel, err := zerolog.ParseLevel(c.LogLevel)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("error setting log_level") errlog.Error().Err(err).Msg("error setting log_level")
} }
zerolog.SetGlobalLevel(logLevel) zerolog.SetGlobalLevel(logLevel)
@ -211,7 +233,7 @@ func initDB(c *config, useDB bool) (*pgx.Conn, error) {
config.LogLevel = pgx.LogLevelNone config.LogLevel = pgx.LogLevelNone
} }
config.Logger = NewSQLLogger(*logger) config.Logger = NewSQLLogger(logger)
db, err := pgx.ConnectConfig(context.Background(), config) db, err := pgx.ConnectConfig(context.Background(), config)
if err != nil { if err != nil {
@ -246,7 +268,7 @@ func initDBPool(c *config) (*pgxpool.Pool, error) {
config.ConnConfig.LogLevel = pgx.LogLevelNone config.ConnConfig.LogLevel = pgx.LogLevelNone
} }
config.ConnConfig.Logger = NewSQLLogger(*logger) config.ConnConfig.Logger = NewSQLLogger(logger)
// if c.DB.MaxRetries != 0 { // if c.DB.MaxRetries != 0 {
// opt.MaxRetries = c.DB.MaxRetries // opt.MaxRetries = c.DB.MaxRetries
@ -269,10 +291,45 @@ func initCompiler() {
qcompile, pcompile, err = initCompilers(conf) qcompile, pcompile, err = initCompilers(conf)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to initialize compilers") errlog.Fatal().Err(err).Msg("failed to initialize compilers")
} }
if err := initResolvers(); err != nil { if err := initResolvers(); err != nil {
logger.Fatal().Err(err).Msg("failed to initialized resolvers") errlog.Fatal().Err(err).Msg("failed to initialized resolvers")
} }
} }
func initConfOnce() {
var err error
if conf == nil {
if conf, err = initConf(); err != nil {
errlog.Fatal().Err(err).Msg("failed to read config")
}
}
}
func cmdVersion(cmd *cobra.Command, args []string) {
fmt.Printf("\n%s\n", BuildDetails())
}
func BuildDetails() string {
return fmt.Sprintf(`
Super Graph %v
Commit SHA-1 : %v
Commit timestamp : %v
Branch : %v
Go version : %v
For documentation, visit https://supergraph.dev
Licensed under the Apache Public License 2.0
Copyright 2015-2019 Vikram Rangnekar.
`,
version,
lastCommitSHA,
lastCommitTime,
gitBranch,
runtime.Version())
}

View File

@ -9,7 +9,7 @@ import (
func cmdConfDump(cmd *cobra.Command, args []string) { func cmdConfDump(cmd *cobra.Command, args []string) {
if len(args) != 1 { if len(args) != 1 {
cmd.Help() cmd.Help() //nolint: errcheck
os.Exit(1) os.Exit(1)
} }
@ -17,11 +17,11 @@ func cmdConfDump(cmd *cobra.Command, args []string) {
conf, err := initConf() conf, err := initConf()
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to read config") errlog.Fatal().Err(err).Msg("failed to read config")
} }
if err := conf.Viper.WriteConfigAs(fname); err != nil { if err := conf.Viper.WriteConfigAs(fname); err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
logger.Info().Msgf("config dumped to ./%s", fname) logger.Info().Msgf("config dumped to ./%s", fname)

View File

@ -14,19 +14,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var sampleMigration = `-- This is a sample migration.
create table users(
id serial primary key,
fullname varchar not null,
email varchar not null
);
---- create above / drop below ----
drop table users;
`
var newMigrationText = `-- Write your migrate up statements here var newMigrationText = `-- Write your migrate up statements here
---- create above / drop below ---- ---- create above / drop below ----
@ -36,6 +23,7 @@ var newMigrationText = `-- Write your migrate up statements here
` `
func cmdDBSetup(cmd *cobra.Command, args []string) { func cmdDBSetup(cmd *cobra.Command, args []string) {
initConfOnce()
cmdDBCreate(cmd, []string{}) cmdDBCreate(cmd, []string{})
cmdDBMigrate(cmd, []string{"up"}) cmdDBMigrate(cmd, []string{"up"})
@ -47,25 +35,31 @@ func cmdDBSetup(cmd *cobra.Command, args []string) {
return return
} }
if os.IsNotExist(err) == false { if !os.IsNotExist(err) {
logger.Fatal().Err(err).Msgf("unable to check if '%s' exists", sfile) errlog.Fatal().Err(err).Msgf("unable to check if '%s' exists", sfile)
} }
logger.Warn().Msgf("failed to read seed file '%s'", sfile) logger.Warn().Msgf("failed to read seed file '%s'", sfile)
} }
func cmdDBCreate(cmd *cobra.Command, args []string) { func cmdDBReset(cmd *cobra.Command, args []string) {
var err error initConfOnce()
if conf, err = initConf(); err != nil { if conf.Production {
logger.Fatal().Err(err).Msg("failed to read config") errlog.Fatal().Msg("db:reset does not work in production")
return
} }
cmdDBDrop(cmd, []string{})
cmdDBSetup(cmd, []string{})
}
func cmdDBCreate(cmd *cobra.Command, args []string) {
initConfOnce()
ctx := context.Background() ctx := context.Background()
conn, err := initDB(conf, false) conn, err := initDB(conf, false)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to database") errlog.Fatal().Err(err).Msg("failed to connect to database")
} }
defer conn.Close(ctx) defer conn.Close(ctx)
@ -73,24 +67,19 @@ func cmdDBCreate(cmd *cobra.Command, args []string) {
_, err = conn.Exec(ctx, sql) _, err = conn.Exec(ctx, sql)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to create database") errlog.Fatal().Err(err).Msg("failed to create database")
} }
logger.Info().Msgf("created database '%s'", conf.DB.DBName) logger.Info().Msgf("created database '%s'", conf.DB.DBName)
} }
func cmdDBDrop(cmd *cobra.Command, args []string) { func cmdDBDrop(cmd *cobra.Command, args []string) {
var err error initConfOnce()
if conf, err = initConf(); err != nil {
logger.Fatal().Err(err).Msg("failed to read config")
}
ctx := context.Background() ctx := context.Background()
conn, err := initDB(conf, false) conn, err := initDB(conf, false)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to database") errlog.Fatal().Err(err).Msg("failed to connect to database")
} }
defer conn.Close(ctx) defer conn.Close(ctx)
@ -98,7 +87,7 @@ func cmdDBDrop(cmd *cobra.Command, args []string) {
_, err = conn.Exec(ctx, sql) _, err = conn.Exec(ctx, sql)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to create database") errlog.Fatal().Err(err).Msg("failed to create database")
} }
logger.Info().Msgf("dropped database '%s'", conf.DB.DBName) logger.Info().Msgf("dropped database '%s'", conf.DB.DBName)
@ -106,16 +95,11 @@ func cmdDBDrop(cmd *cobra.Command, args []string) {
func cmdDBNew(cmd *cobra.Command, args []string) { func cmdDBNew(cmd *cobra.Command, args []string) {
if len(args) != 1 { if len(args) != 1 {
cmd.Help() cmd.Help() //nolint: errcheck
os.Exit(1) os.Exit(1)
} }
var err error initConfOnce()
if conf, err = initConf(); err != nil {
logger.Fatal().Err(err).Msg("failed to read config")
}
name := args[0] name := args[0]
m, err := migrate.FindMigrations(conf.MigrationsPath) m, err := migrate.FindMigrations(conf.MigrationsPath)
@ -124,7 +108,7 @@ func cmdDBNew(cmd *cobra.Command, args []string) {
os.Exit(1) os.Exit(1)
} }
mname := fmt.Sprintf("%03d_%s.sql", (len(m) + 1), name) mname := fmt.Sprintf("%d_%s.sql", len(m), name)
// Write new migration // Write new migration
mpath := filepath.Join(conf.MigrationsPath, mname) mpath := filepath.Join(conf.MigrationsPath, mname)
@ -144,39 +128,34 @@ func cmdDBNew(cmd *cobra.Command, args []string) {
} }
func cmdDBMigrate(cmd *cobra.Command, args []string) { func cmdDBMigrate(cmd *cobra.Command, args []string) {
var err error
if len(args) == 0 { if len(args) == 0 {
cmd.Help() cmd.Help() //nolint: errcheck
os.Exit(1) os.Exit(1)
} }
initConfOnce()
dest := args[0] dest := args[0]
if conf, err = initConf(); err != nil {
logger.Fatal().Err(err).Msg("failed to read config")
}
conn, err := initDB(conf, true) conn, err := initDB(conf, true)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to database") errlog.Fatal().Err(err).Msg("failed to connect to database")
} }
defer conn.Close(context.Background()) defer conn.Close(context.Background())
m, err := migrate.NewMigrator(conn, "schema_version") m, err := migrate.NewMigrator(conn, "schema_version")
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to initializing migrator") errlog.Fatal().Err(err).Msg("failed to initializing migrator")
} }
m.Data = getMigrationVars() m.Data = getMigrationVars()
err = m.LoadMigrations(conf.MigrationsPath) err = m.LoadMigrations(conf.MigrationsPath)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to load migrations") errlog.Fatal().Err(err).Msg("failed to load migrations")
} }
if len(m.Migrations) == 0 { if len(m.Migrations) == 0 {
logger.Fatal().Msg("No migrations found") errlog.Fatal().Msg("No migrations found")
} }
m.OnStart = func(sequence int32, name, direction, sql string) { m.OnStart = func(sequence int32, name, direction, sql string) {
@ -195,7 +174,7 @@ func cmdDBMigrate(cmd *cobra.Command, args []string) {
var n int64 var n int64
n, err = strconv.ParseInt(d, 10, 32) n, err = strconv.ParseInt(d, 10, 32)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("invalid destination") errlog.Fatal().Err(err).Msg("invalid destination")
} }
return int32(n) return int32(n)
} }
@ -219,24 +198,22 @@ func cmdDBMigrate(cmd *cobra.Command, args []string) {
err = m.MigrateTo(currentVersion + mustParseDestination(dest[1:])) err = m.MigrateTo(currentVersion + mustParseDestination(dest[1:]))
} else { } else {
cmd.Help() cmd.Help() //nolint: errcheck
os.Exit(1) os.Exit(1)
} }
if err != nil { if err != nil {
logger.Info().Err(err).Send() logger.Info().Err(err).Send()
// logger.Info().Err(err).Send()
// if err, ok := err.(m.MigrationPgError); ok { // if err, ok := err.(m.MigrationPgError); ok {
// if err.Detail != "" { // if err.Detail != "" {
// logger.Info().Err(err).Msg(err.Detail) // info.Err(err).Msg(err.Detail)
// } // }
// if err.Position != 0 { // if err.Position != 0 {
// ele, err := ExtractErrorLine(err.Sql, int(err.Position)) // ele, err := ExtractErrorLine(err.Sql, int(err.Position))
// if err != nil { // if err != nil {
// logger.Fatal().Err(err).Send() // errlog.Fatal().Err(err).Send()
// } // }
// prefix := fmt.Sprintf() // prefix := fmt.Sprintf()
@ -251,37 +228,33 @@ func cmdDBMigrate(cmd *cobra.Command, args []string) {
} }
func cmdDBStatus(cmd *cobra.Command, args []string) { func cmdDBStatus(cmd *cobra.Command, args []string) {
var err error initConfOnce()
if conf, err = initConf(); err != nil {
logger.Fatal().Err(err).Msg("failed to read config")
}
conn, err := initDB(conf, true) conn, err := initDB(conf, true)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to database") errlog.Fatal().Err(err).Msg("failed to connect to database")
} }
defer conn.Close(context.Background()) defer conn.Close(context.Background())
m, err := migrate.NewMigrator(conn, "schema_version") m, err := migrate.NewMigrator(conn, "schema_version")
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to initialize migrator") errlog.Fatal().Err(err).Msg("failed to initialize migrator")
} }
m.Data = getMigrationVars() m.Data = getMigrationVars()
err = m.LoadMigrations(conf.MigrationsPath) err = m.LoadMigrations(conf.MigrationsPath)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to load migrations") errlog.Fatal().Err(err).Msg("failed to load migrations")
} }
if len(m.Migrations) == 0 { if len(m.Migrations) == 0 {
logger.Fatal().Msg("no migrations found") errlog.Fatal().Msg("no migrations found")
} }
mver, err := m.GetCurrentVersion() mver, err := m.GetCurrentVersion()
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to retrieve migration") errlog.Fatal().Err(err).Msg("failed to retrieve migration")
} }
var status string var status string

View File

@ -16,7 +16,7 @@ import (
func cmdNew(cmd *cobra.Command, args []string) { func cmdNew(cmd *cobra.Command, args []string) {
if len(args) != 1 { if len(args) != 1 {
cmd.Help() cmd.Help() //nolint: errcheck
os.Exit(1) os.Exit(1)
} }
@ -115,13 +115,17 @@ func (t *Templ) get(name string) ([]byte, error) {
b := bytes.Buffer{} b := bytes.Buffer{}
tmpl := fasttemplate.New(v, "{%", "%}") tmpl := fasttemplate.New(v, "{%", "%}")
tmpl.ExecuteFunc(&b, func(w io.Writer, tag string) (int, error) { _, err := tmpl.ExecuteFunc(&b, func(w io.Writer, tag string) (int, error) {
if val, ok := t.data[strings.TrimSpace(tag)]; ok { if val, ok := t.data[strings.TrimSpace(tag)]; ok {
return w.Write([]byte(val)) return w.Write([]byte(val))
} }
return 0, fmt.Errorf("unknown template variable '%s'", tag) return 0, fmt.Errorf("unknown template variable '%s'", tag)
}) })
if err != nil {
return nil, err
}
return b.Bytes(), nil return b.Bytes(), nil
} }
@ -133,13 +137,13 @@ func ifNotExists(filePath string, doFn func(string) error) {
return return
} }
if os.IsNotExist(err) == false { if !os.IsNotExist(err) {
logger.Fatal().Err(err).Msgf("unable to check if '%s' exists", filePath) errlog.Fatal().Err(err).Msgf("unable to check if '%s' exists", filePath)
} }
err = doFn(filePath) err = doFn(filePath)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msgf("unable to create '%s'", filePath) errlog.Fatal().Err(err).Msgf("unable to create '%s'", filePath)
} }
logger.Info().Msgf("created '%s'", filePath) logger.Info().Msgf("created '%s'", filePath)

View File

@ -1,6 +1,7 @@
package serv package serv
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -12,20 +13,21 @@ import (
"github.com/brianvoe/gofakeit" "github.com/brianvoe/gofakeit"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/valyala/fasttemplate"
) )
func cmdDBSeed(cmd *cobra.Command, args []string) { func cmdDBSeed(cmd *cobra.Command, args []string) {
var err error var err error
if conf, err = initConf(); err != nil { if conf, err = initConf(); err != nil {
logger.Fatal().Err(err).Msg("failed to read config") errlog.Fatal().Err(err).Msg("failed to read config")
} }
conf.Production = false conf.Production = false
db, err = initDBPool(conf) db, err = initDBPool(conf)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to database") errlog.Fatal().Err(err).Msg("failed to connect to database")
} }
initCompiler() initCompiler()
@ -34,14 +36,14 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
b, err := ioutil.ReadFile(path.Join(confPath, conf.SeedFile)) b, err := ioutil.ReadFile(path.Join(confPath, conf.SeedFile))
if err != nil { if err != nil {
logger.Fatal().Err(err).Msgf("failed to read seed file '%s'", sfile) errlog.Fatal().Err(err).Msgf("failed to read seed file '%s'", sfile)
} }
vm := goja.New() vm := goja.New()
vm.Set("graphql", graphQLFunc) vm.Set("graphql", graphQLFunc)
console := vm.NewObject() console := vm.NewObject()
console.Set("log", logFunc) console.Set("log", logFunc) //nolint: errcheck
vm.Set("console", console) vm.Set("console", console)
fake := vm.NewObject() fake := vm.NewObject()
@ -50,39 +52,83 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
_, err = vm.RunScript("seed.js", string(b)) _, err = vm.RunScript("seed.js", string(b))
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to execute script") errlog.Fatal().Err(err).Msg("failed to execute script")
} }
logger.Info().Msg("seed script done") logger.Info().Msg("seed script done")
} }
//func runFunc(call goja.FunctionCall) { //func runFunc(call goja.FunctionCall) {
func graphQLFunc(query string, data interface{}) map[string]interface{} { func graphQLFunc(query string, data interface{}, opt map[string]string) map[string]interface{} {
b, err := json.Marshal(data) vars, err := json.Marshal(data)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to json serialize") errlog.Fatal().Err(err).Send()
} }
c := &coreContext{Context: context.Background()} c := context.Background()
c.req.Query = query
c.req.Vars = b
c.req.role = "user"
res, err := c.execQuery() if v, ok := opt["user_id"]; ok && len(v) != 0 {
c = context.WithValue(c, userIDKey, v)
}
var role string
if v, ok := opt["role"]; ok && len(v) != 0 {
role = v
} else {
role = "user"
}
stmts, err := buildRoleStmt([]byte(query), vars, role)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("graphql query failed") errlog.Fatal().Err(err).Msg("graphql query failed")
}
st := stmts[0]
buf := &bytes.Buffer{}
t := fasttemplate.New(st.sql, openVar, closeVar)
_, err = t.ExecuteFunc(buf, argMap(c, vars))
if err != nil {
errlog.Fatal().Err(err).Send()
}
finalSQL := buf.String()
tx, err := db.Begin(c)
if err != nil {
errlog.Fatal().Err(err).Send()
}
defer tx.Rollback(c) //nolint: errcheck
if conf.DB.SetUserID {
if err := setLocalUserID(c, tx); err != nil {
errlog.Fatal().Err(err).Send()
}
}
var root []byte
if err = tx.QueryRow(c, finalSQL).Scan(&root); err != nil {
errlog.Fatal().Err(err).Msg("sql query failed")
}
if err := tx.Commit(c); err != nil {
errlog.Fatal().Err(err).Send()
} }
val := make(map[string]interface{}) val := make(map[string]interface{})
err = json.Unmarshal(res, &val) err = json.Unmarshal(root, &val)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to deserialize json") errlog.Fatal().Err(err).Send()
} }
return val return val
} }
//nolint: errcheck
func logFunc(args ...interface{}) { func logFunc(args ...interface{}) {
for _, arg := range args { for _, arg := range args {
if _, ok := arg.(map[string]interface{}); ok { if _, ok := arg.(map[string]interface{}); ok {
@ -99,6 +145,7 @@ func logFunc(args ...interface{}) {
} }
} }
//nolint: errcheck
func setFakeFuncs(f *goja.Object) { func setFakeFuncs(f *goja.Object) {
gofakeit.Seed(0) gofakeit.Seed(0)
@ -156,10 +203,9 @@ func setFakeFuncs(f *goja.Object) {
f.Set("transmission_gear_type", gofakeit.TransmissionGearType) f.Set("transmission_gear_type", gofakeit.TransmissionGearType)
// Text // Text
f.Set("word", gofakeit.Word) f.Set("word", gofakeit.Word)
f.Set("sentence", gofakeit.Sentence) f.Set("sentence", gofakeit.Sentence)
f.Set("paragrph", gofakeit.Paragraph) f.Set("paragraph", gofakeit.Paragraph)
f.Set("question", gofakeit.Question) f.Set("question", gofakeit.Question)
f.Set("quote", gofakeit.Quote) f.Set("quote", gofakeit.Quote)

View File

@ -8,12 +8,12 @@ func cmdServ(cmd *cobra.Command, args []string) {
var err error var err error
if conf, err = initConf(); err != nil { if conf, err = initConf(); err != nil {
logger.Fatal().Err(err).Msg("failed to read config") errlog.Fatal().Err(err).Msg("failed to read config")
} }
db, err = initDBPool(conf) db, err = initDBPool(conf)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to database") errlog.Fatal().Err(err).Msg("failed to connect to database")
} }
initCompiler() initCompiler()

View File

@ -64,17 +64,12 @@ type config struct {
User string User string
Password string Password string
Schema string Schema string
PoolSize int32 `mapstructure:"pool_size"` PoolSize int32 `mapstructure:"pool_size"`
MaxRetries int `mapstructure:"max_retries"` MaxRetries int `mapstructure:"max_retries"`
LogLevel string `mapstructure:"log_level"` SetUserID bool `mapstructure:"set_user_id"`
SetUserID bool `mapstructure:"set_user_id"`
Vars map[string]string `mapstructure:"variables"` Vars map[string]string `mapstructure:"variables"`
Blocklist []string
Defaults struct {
Filters []string
Blocklist []string
}
Tables []configTable Tables []configTable
} `mapstructure:"database"` } `mapstructure:"database"`
@ -83,6 +78,7 @@ type config struct {
RolesQuery string `mapstructure:"roles_query"` RolesQuery string `mapstructure:"roles_query"`
Roles []configRole Roles []configRole
roles map[string]*configRole
} }
type configTable struct { type configTable struct {
@ -177,9 +173,10 @@ func newConfig(name string) *viper.Viper {
vi.SetDefault("database.schema", "public") vi.SetDefault("database.schema", "public")
vi.SetDefault("env", "development") vi.SetDefault("env", "development")
vi.BindEnv("env", "GO_ENV")
vi.BindEnv("HOST", "HOST") vi.BindEnv("env", "GO_ENV") //nolint: errcheck
vi.BindEnv("PORT", "PORT") vi.BindEnv("HOST", "HOST") //nolint: errcheck
vi.BindEnv("PORT", "PORT") //nolint: errcheck
vi.SetDefault("auth.rails.max_idle", 80) vi.SetDefault("auth.rails.max_idle", 80)
vi.SetDefault("auth.rails.max_active", 12000) vi.SetDefault("auth.rails.max_active", 12000)
@ -221,16 +218,15 @@ func (c *config) Init(vi *viper.Viper) error {
} }
c.RolesQuery = sanitize(c.RolesQuery) c.RolesQuery = sanitize(c.RolesQuery)
c.roles = make(map[string]*configRole)
rolesMap := make(map[string]struct{})
for i := range c.Roles { for i := range c.Roles {
role := &c.Roles[i] role := &c.Roles[i]
if _, ok := rolesMap[role.Name]; ok { if _, ok := c.roles[role.Name]; ok {
logger.Fatal().Msgf("duplicate role '%s' found", role.Name) errlog.Fatal().Msgf("duplicate role '%s' found", role.Name)
} }
role.Name = sanitize(role.Name) role.Name = strings.ToLower(role.Name)
role.Match = sanitize(role.Match) role.Match = sanitize(role.Match)
role.tablesMap = make(map[string]*configRoleTable) role.tablesMap = make(map[string]*configRoleTable)
@ -238,14 +234,16 @@ func (c *config) Init(vi *viper.Viper) error {
role.tablesMap[table.Name] = &role.Tables[n] role.tablesMap[table.Name] = &role.Tables[n]
} }
rolesMap[role.Name] = struct{}{} c.roles[role.Name] = role
} }
if _, ok := rolesMap["user"]; !ok { if _, ok := c.roles["user"]; !ok {
c.Roles = append(c.Roles, configRole{Name: "user"}) u := configRole{Name: "user"}
c.Roles = append(c.Roles, u)
c.roles["user"] = &u
} }
if _, ok := rolesMap["anon"]; !ok { if _, ok := c.roles["anon"]; !ok {
logger.Warn().Msg("unauthenticated requests will be blocked. no role 'anon' defined") logger.Warn().Msg("unauthenticated requests will be blocked. no role 'anon' defined")
c.AuthFailBlock = true c.AuthFailBlock = true
} }
@ -262,7 +260,7 @@ func (c *config) validate() {
name := c.Roles[i].Name name := c.Roles[i].Name
if _, ok := rm[name]; ok { if _, ok := rm[name]; ok {
logger.Fatal().Msgf("duplicate config for role '%s'", c.Roles[i].Name) errlog.Fatal().Msgf("duplicate config for role '%s'", c.Roles[i].Name)
} }
rm[name] = struct{}{} rm[name] = struct{}{}
} }
@ -273,7 +271,7 @@ func (c *config) validate() {
name := c.Tables[i].Name name := c.Tables[i].Name
if _, ok := tm[name]; ok { if _, ok := tm[name]; ok {
logger.Fatal().Msgf("duplicate config for table '%s'", c.Tables[i].Name) errlog.Fatal().Msgf("duplicate config for table '%s'", c.Tables[i].Name)
} }
tm[name] = struct{}{} tm[name] = struct{}{}
} }

View File

@ -8,20 +8,14 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/cespare/xxhash/v2" "github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/jsn"
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
"github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
) )
const (
empty = ""
)
type coreContext struct { type coreContext struct {
req gqlReq req gqlReq
res gqlResp res gqlResp
@ -32,6 +26,10 @@ func (c *coreContext) handleReq(w io.Writer, req *http.Request) error {
c.req.ref = req.Referer() c.req.ref = req.Referer()
c.req.hdr = req.Header c.req.hdr = req.Header
if len(c.req.Vars) == 2 {
c.req.Vars = nil
}
if authCheck(c) { if authCheck(c) {
c.req.role = "user" c.req.role = "user"
} else { } else {
@ -47,90 +45,55 @@ func (c *coreContext) handleReq(w io.Writer, req *http.Request) error {
} }
func (c *coreContext) execQuery() ([]byte, error) { func (c *coreContext) execQuery() ([]byte, error) {
var err error
var skipped uint32
var qc *qcode.QCode
var data []byte var data []byte
var st *stmt
logger.Debug().Str("role", c.req.role).Msg(c.req.Query) var err error
if conf.Production { if conf.Production {
var ps *preparedItem data, st, err = c.resolvePreparedSQL()
data, ps, err = c.resolvePreparedSQL()
if err != nil { if err != nil {
return nil, err logger.Error().
} Err(err).
Str("default_role", c.req.role).
Msg(c.req.Query)
skipped = ps.skipped return nil, errors.New("query failed. check logs for error")
qc = ps.qc }
} else { } else {
if data, st, err = c.resolveSQL(); err != nil {
data, skipped, err = c.resolveSQL()
if err != nil {
return nil, err return nil, err
} }
} }
if len(data) == 0 || skipped == 0 { return execRemoteJoin(st, data, c.req.hdr)
return data, nil
}
sel := qc.Selects
h := xxhash.New()
// fetch the field name used within the db response json
// that are used to mark insertion points and the mapping between
// those field names and their select objects
fids, sfmap := parentFieldIds(h, sel, skipped)
// fetch the field values of the marked insertion points
// these values contain the id to be used with fetching remote data
from := jsn.Get(data, fids)
var to []jsn.Field
switch {
case len(from) == 1:
to, err = c.resolveRemote(c.req.hdr, h, from[0], sel, sfmap)
case len(from) > 1:
to, err = c.resolveRemotes(c.req.hdr, h, from, sel, sfmap)
default:
return nil, errors.New("something wrong no remote ids found in db response")
}
if err != nil {
return nil, err
}
var ob bytes.Buffer
err = jsn.Replace(&ob, data, from, to)
if err != nil {
return nil, err
}
return ob.Bytes(), nil
} }
func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) { func (c *coreContext) resolvePreparedSQL() ([]byte, *stmt, error) {
tx, err := db.Begin(c) var tx pgx.Tx
if err != nil { var err error
return nil, nil, err
qt := qcode.GetQType(c.req.Query)
mutation := (qt == qcode.QTMutation)
anonQuery := (qt == qcode.QTQuery && c.req.role == "anon")
useRoleQuery := len(conf.RolesQuery) != 0 && mutation
useTx := useRoleQuery || conf.DB.SetUserID
if useTx {
if tx, err = db.Begin(c); err != nil {
return nil, nil, err
}
defer tx.Rollback(c) //nolint: errcheck
} }
defer tx.Rollback(c)
if conf.DB.SetUserID { if conf.DB.SetUserID {
if err := c.setLocalUserID(tx); err != nil { if err := setLocalUserID(c, tx); err != nil {
return nil, nil, err return nil, nil, err
} }
} }
var role string var role string
mutation := isMutation(c.req.Query)
useRoleQuery := len(conf.RolesQuery) != 0 && mutation
if useRoleQuery { if useRoleQuery {
if role, err = c.executeRoleQuery(tx); err != nil { if role, err = c.executeRoleQuery(tx); err != nil {
@ -140,7 +103,7 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) {
} else if v := c.Value(userRoleKey); v != nil { } else if v := c.Value(userRoleKey); v != nil {
role = v.(string) role = v.(string)
} else if mutation { } else {
role = c.req.role role = c.req.role
} }
@ -151,69 +114,92 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) {
} }
var root []byte var root []byte
vars := argList(c, ps.args) var row pgx.Row
if mutation { vars, err := argList(c, ps.args)
err = tx.QueryRow(c, ps.stmt.SQL, vars...).Scan(&root)
} else {
err = tx.QueryRow(c, ps.stmt.SQL, vars...).Scan(&c.req.role, &root)
}
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if err := tx.Commit(c); err != nil { if useTx {
row = tx.QueryRow(c, ps.sd.SQL, vars...)
} else {
row = db.QueryRow(c, ps.sd.SQL, vars...)
}
if mutation || anonQuery {
err = row.Scan(&root)
} else {
err = row.Scan(&role, &root)
}
if len(role) == 0 {
logger.Debug().Str("default_role", c.req.role).Msg(c.req.Query)
} else {
logger.Debug().Str("default_role", c.req.role).Str("role", role).Msg(c.req.Query)
}
if err != nil {
return nil, nil, err return nil, nil, err
} }
return root, ps, nil c.req.role = role
if useTx {
if err := tx.Commit(c); err != nil {
return nil, nil, err
}
}
return root, ps.st, nil
} }
func (c *coreContext) resolveSQL() ([]byte, uint32, error) { func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
tx, err := db.Begin(c) var tx pgx.Tx
if err != nil { var err error
return nil, 0, err
} qt := qcode.GetQType(c.req.Query)
defer tx.Rollback(c) mutation := (qt == qcode.QTMutation)
//anonQuery := (qt == qcode.QTQuery && c.req.role == "anon")
mutation := isMutation(c.req.Query)
useRoleQuery := len(conf.RolesQuery) != 0 && mutation useRoleQuery := len(conf.RolesQuery) != 0 && mutation
useTx := useRoleQuery || conf.DB.SetUserID
if useTx {
if tx, err = db.Begin(c); err != nil {
return nil, nil, err
}
defer tx.Rollback(c) //nolint: errcheck
}
if conf.DB.SetUserID {
if err := setLocalUserID(c, tx); err != nil {
return nil, nil, err
}
}
if useRoleQuery { if useRoleQuery {
if c.req.role, err = c.executeRoleQuery(tx); err != nil { if c.req.role, err = c.executeRoleQuery(tx); err != nil {
return nil, 0, err return nil, nil, err
} }
} else if v := c.Value(userRoleKey); v != nil { } else if v := c.Value(userRoleKey); v != nil {
c.req.role = v.(string) c.req.role = v.(string)
} }
stmts, err := c.buildStmt() stmts, err := buildStmt(qt, []byte(c.req.Query), c.req.Vars, c.req.role)
if err != nil { if err != nil {
return nil, 0, err return nil, nil, err
}
var st *stmt
if mutation {
st = findStmt(c.req.role, stmts)
} else {
st = &stmts[0]
} }
st := &stmts[0]
t := fasttemplate.New(st.sql, openVar, closeVar) t := fasttemplate.New(st.sql, openVar, closeVar)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
_, err = t.ExecuteFunc(buf, argMap(c))
if err == errNoUserID {
logger.Warn().Msg("no user id found. query requires an authenicated request")
}
_, err = t.ExecuteFunc(buf, argMap(c, c.req.Vars))
if err != nil { if err != nil {
return nil, 0, err return nil, nil, err
} }
finalSQL := buf.String() finalSQL := buf.String()
var stime time.Time var stime time.Time
@ -222,192 +208,57 @@ func (c *coreContext) resolveSQL() ([]byte, uint32, error) {
stime = time.Now() stime = time.Now()
} }
if conf.DB.SetUserID { var root []byte
if err := c.setLocalUserID(tx); err != nil { var role string
return nil, 0, err var row pgx.Row
defaultRole := c.req.role
if useTx {
row = tx.QueryRow(c, finalSQL)
} else {
row = db.QueryRow(c, finalSQL)
}
if len(stmts) == 1 {
err = row.Scan(&root)
} else {
err = row.Scan(&role, &root)
}
if len(role) == 0 {
logger.Debug().Str("default_role", defaultRole).Msg(c.req.Query)
} else {
logger.Debug().Str("default_role", defaultRole).Str("role", role).Msg(c.req.Query)
}
if err != nil {
return nil, nil, err
}
if useTx {
if err := tx.Commit(c); err != nil {
return nil, nil, err
} }
} }
var root []byte if !conf.Production {
if mutation {
err = tx.QueryRow(c, finalSQL).Scan(&root)
} else {
err = tx.QueryRow(c, finalSQL).Scan(&c.req.role, &root)
}
if err != nil {
return nil, 0, err
}
if err := tx.Commit(c); err != nil {
return nil, 0, err
}
if mutation {
st = findStmt(c.req.role, stmts)
} else {
st = &stmts[0]
}
if conf.EnableTracing && len(st.qc.Selects) != 0 {
c.addTrace(
st.qc.Selects,
st.qc.Selects[0].ID,
stime)
}
if conf.Production == false {
_allowList.add(&c.req) _allowList.add(&c.req)
} }
return root, st.skipped, nil if len(stmts) > 1 {
} if st = findStmt(role, stmts); st == nil {
return nil, nil, fmt.Errorf("invalid role '%s' returned", role)
func (c *coreContext) resolveRemote( }
hdr http.Header,
h *xxhash.Digest,
field jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
// replacement data for the marked insertion points
// key and value will be replaced by whats below
toA := [1]jsn.Field{}
to := toA[:1]
// use the json key to find the related Select object
k1 := xxhash.Sum64(field.Key)
s, ok := sfmap[k1]
if !ok {
return nil, nil
}
p := sel[s.ParentID]
// then use the Table nme in the Select and it's parent
// to find the resolver to use for this relationship
k2 := mkkey(h, s.Table, p.Table)
r, ok := rmap[k2]
if !ok {
return nil, nil
}
id := jsn.Value(field.Value)
if len(id) == 0 {
return nil, nil
}
st := time.Now()
b, err := r.Fn(hdr, id)
if err != nil {
return nil, err
} }
if conf.EnableTracing { if conf.EnableTracing {
c.addTrace(sel, s.ID, st) for _, id := range st.qc.Roots {
c.addTrace(st.qc.Selects, id, stime)
}
} }
if len(r.Path) != 0 { return root, st, nil
b = jsn.Strip(b, r.Path)
}
var ob bytes.Buffer
if len(s.Cols) != 0 {
err = jsn.Filter(&ob, b, colsToList(s.Cols))
if err != nil {
return nil, err
}
} else {
ob.WriteString("null")
}
to[0] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
return to, nil
}
func (c *coreContext) resolveRemotes(
hdr http.Header,
h *xxhash.Digest,
from []jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
// replacement data for the marked insertion points
// key and value will be replaced by whats below
to := make([]jsn.Field, len(from))
var wg sync.WaitGroup
wg.Add(len(from))
var cerr error
for i, id := range from {
// use the json key to find the related Select object
k1 := xxhash.Sum64(id.Key)
s, ok := sfmap[k1]
if !ok {
return nil, nil
}
p := sel[s.ParentID]
// then use the Table nme in the Select and it's parent
// to find the resolver to use for this relationship
k2 := mkkey(h, s.Table, p.Table)
r, ok := rmap[k2]
if !ok {
return nil, nil
}
id := jsn.Value(id.Value)
if len(id) == 0 {
return nil, nil
}
go func(n int, id []byte, s *qcode.Select) {
defer wg.Done()
st := time.Now()
b, err := r.Fn(hdr, id)
if err != nil {
cerr = fmt.Errorf("%s: %s", s.Table, err)
return
}
if conf.EnableTracing {
c.addTrace(sel, s.ID, st)
}
if len(r.Path) != 0 {
b = jsn.Strip(b, r.Path)
}
var ob bytes.Buffer
if len(s.Cols) != 0 {
err = jsn.Filter(&ob, b, colsToList(s.Cols))
if err != nil {
cerr = fmt.Errorf("%s: %s", s.Table, err)
return
}
} else {
ob.WriteString("null")
}
to[n] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
}(i, id, s)
}
wg.Wait()
return to, cerr
} }
func (c *coreContext) executeRoleQuery(tx pgx.Tx) (string, error) { func (c *coreContext) executeRoleQuery(tx pgx.Tx) (string, error) {
@ -421,15 +272,6 @@ func (c *coreContext) executeRoleQuery(tx pgx.Tx) (string, error) {
return role, nil return role, nil
} }
func (c *coreContext) setLocalUserID(tx pgx.Tx) error {
var err error
if v := c.Value(userIDKey); v != nil {
_, err = tx.Exec(c, fmt.Sprintf(`SET LOCAL "user.id" = %s;`, v))
}
return err
}
func (c *coreContext) render(w io.Writer, data []byte) error { func (c *coreContext) render(w io.Writer, data []byte) error {
c.res.Data = json.RawMessage(data) c.res.Data = json.RawMessage(data)
return json.NewEncoder(w).Encode(c.res) return json.NewEncoder(w).Encode(c.res)
@ -451,7 +293,7 @@ func (c *coreContext) addTrace(sel []qcode.Select, id int32, st time.Time) {
c.res.Extensions.Tracing.Duration = du c.res.Extensions.Tracing.Duration = du
n := 1 n := 1
for i := id; i != 0; i = sel[i].ParentID { for i := id; i != -1; i = sel[i].ParentID {
n++ n++
} }
path := make([]string, n) path := make([]string, n)
@ -459,7 +301,7 @@ func (c *coreContext) addTrace(sel []qcode.Select, id int32, st time.Time) {
n-- n--
for i := id; ; i = sel[i].ParentID { for i := id; ; i = sel[i].ParentID {
path[n] = sel[i].Table path[n] = sel[i].Table
if sel[i].ID == 0 { if sel[i].ParentID == -1 {
break break
} }
n-- n--
@ -502,7 +344,7 @@ func parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) (
for i := range sel { for i := range sel {
s := &sel[i] s := &sel[i]
if isSkipped(skipped, uint32(s.ID)) == false { if !isSkipped(skipped, uint32(s.ID)) {
continue continue
} }
@ -521,6 +363,15 @@ func parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) (
return fm, sm return fm, sm
} }
func setLocalUserID(c context.Context, tx pgx.Tx) error {
var err error
if v := c.Value(userIDKey); v != nil {
_, err = tx.Exec(c, fmt.Sprintf(`SET LOCAL "user.id" = %s;`, v))
}
return err
}
func isSkipped(n uint32, pos uint32) bool { func isSkipped(n uint32, pos uint32) bool {
return ((n & (1 << pos)) != 0) return ((n & (1 << pos)) != 0)
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"github.com/dosco/super-graph/psql" "github.com/dosco/super-graph/psql"
@ -17,133 +18,170 @@ type stmt struct {
sql string sql string
} }
func (c *coreContext) buildStmt() ([]stmt, error) { func buildStmt(qt qcode.QType, gql, vars []byte, role string) ([]stmt, error) {
var vars map[string]json.RawMessage switch qt {
case qcode.QTMutation:
return buildRoleStmt(gql, vars, role)
if len(c.req.Vars) != 0 { case qcode.QTQuery:
if err := json.Unmarshal(c.req.Vars, &vars); err != nil { switch {
case role == "anon":
return buildRoleStmt(gql, vars, role)
default:
return buildMultiStmt(gql, vars)
}
default:
return nil, fmt.Errorf("unknown query type '%d'", qt)
}
}
func buildRoleStmt(gql, vars []byte, role string) ([]stmt, error) {
ro, ok := conf.roles[role]
if !ok {
return nil, fmt.Errorf(`roles '%s' not defined in config`, role)
}
var vm map[string]json.RawMessage
var err error
if len(vars) != 0 {
if err := json.Unmarshal(vars, &vm); err != nil {
return nil, err return nil, err
} }
} }
gql := []byte(c.req.Query) qc, err := qcompile.Compile(gql, ro.Name)
if len(conf.Roles) == 0 {
return nil, errors.New(`no roles found ('user' and 'anon' required)`)
}
qc, err := qcompile.Compile(gql, conf.Roles[0].Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
stmts := make([]stmt, 0, len(conf.Roles)) // For the 'anon' role in production only compile
mutation := (qc.Type != qcode.QTQuery) // queries for tables defined in the config file.
if conf.Production && ro.Name == "anon" && !hasTablesWithConfig(qc, ro) {
return nil, errors.New("query contains tables with no 'anon' role config")
}
stmts := []stmt{stmt{role: ro, qc: qc}}
w := &bytes.Buffer{} w := &bytes.Buffer{}
for i := 1; i < len(conf.Roles); i++ { skipped, err := pcompile.Compile(qc, w, psql.Variables(vm))
if err != nil {
return nil, err
}
stmts[0].skipped = skipped
stmts[0].sql = w.String()
return stmts, nil
}
func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
var vm map[string]json.RawMessage
var err error
if len(vars) != 0 {
if err := json.Unmarshal(vars, &vm); err != nil {
return nil, err
}
}
if len(conf.RolesQuery) == 0 {
return buildRoleStmt(gql, vars, "user")
}
stmts := make([]stmt, 0, len(conf.Roles))
w := &bytes.Buffer{}
for i := 0; i < len(conf.Roles); i++ {
role := &conf.Roles[i] role := &conf.Roles[i]
// For mutations only render sql for a single role from the request qc, err := qcompile.Compile(gql, role.Name)
if mutation && len(c.req.role) != 0 && role.Name != c.req.role {
continue
}
qc, err = qcompile.Compile(gql, role.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if conf.Production && role.Name == "anon" {
if _, ok := role.tablesMap[qc.Selects[0].Table]; !ok {
continue
}
}
stmts = append(stmts, stmt{role: role, qc: qc}) stmts = append(stmts, stmt{role: role, qc: qc})
if mutation { skipped, err := pcompile.Compile(qc, w, psql.Variables(vm))
skipped, err := pcompile.Compile(qc, w, psql.Variables(vars)) if err != nil {
if err != nil { return nil, err
return nil, err
}
s := &stmts[len(stmts)-1]
s.skipped = skipped
s.sql = w.String()
w.Reset()
} }
s := &stmts[len(stmts)-1]
s.skipped = skipped
s.sql = w.String()
w.Reset()
} }
if mutation { sql, err := renderUserQuery(stmts, vm)
return stmts, nil if err != nil {
return nil, err
} }
stmts[0].sql = sql
return stmts, nil
}
//nolint: errcheck
func renderUserQuery(
stmts []stmt, vars map[string]json.RawMessage) (string, error) {
var err error
w := &bytes.Buffer{}
io.WriteString(w, `SELECT "_sg_auth_info"."role", (CASE "_sg_auth_info"."role" `) io.WriteString(w, `SELECT "_sg_auth_info"."role", (CASE "_sg_auth_info"."role" `)
for _, s := range stmts { for _, s := range stmts {
if len(s.role.Match) == 0 &&
s.role.Name != "user" && s.role.Name != "anon" {
continue
}
io.WriteString(w, `WHEN '`) io.WriteString(w, `WHEN '`)
io.WriteString(w, s.role.Name) io.WriteString(w, s.role.Name)
io.WriteString(w, `' THEN (`) io.WriteString(w, `' THEN (`)
s.skipped, err = pcompile.Compile(s.qc, w, psql.Variables(vars)) s.skipped, err = pcompile.Compile(s.qc, w, psql.Variables(vars))
if err != nil { if err != nil {
return nil, err return "", err
} }
io.WriteString(w, `) `) io.WriteString(w, `) `)
} }
io.WriteString(w, `END) FROM (`)
if len(conf.RolesQuery) == 0 { io.WriteString(w, `END) FROM (SELECT (CASE WHEN EXISTS (`)
v := c.Value(userRoleKey) io.WriteString(w, conf.RolesQuery)
io.WriteString(w, `) THEN `)
io.WriteString(w, `VALUES ("`) io.WriteString(w, `(SELECT (CASE`)
if v != nil { for _, s := range stmts {
io.WriteString(w, v.(string)) if len(s.role.Match) == 0 {
} else { continue
io.WriteString(w, c.req.role)
} }
io.WriteString(w, `")) AS "_sg_auth_info"(role) LIMIT 1;`) io.WriteString(w, ` WHEN `)
io.WriteString(w, s.role.Match)
} else { io.WriteString(w, ` THEN '`)
io.WriteString(w, s.role.Name)
io.WriteString(w, `SELECT (CASE WHEN EXISTS (`) io.WriteString(w, `'`)
io.WriteString(w, conf.RolesQuery)
io.WriteString(w, `) THEN `)
io.WriteString(w, `(SELECT (CASE`)
for _, s := range stmts {
if len(s.role.Match) == 0 {
continue
}
io.WriteString(w, ` WHEN `)
io.WriteString(w, s.role.Match)
io.WriteString(w, ` THEN '`)
io.WriteString(w, s.role.Name)
io.WriteString(w, `'`)
}
if len(c.req.role) == 0 {
io.WriteString(w, ` ELSE 'anon' END) FROM (`)
} else {
io.WriteString(w, ` ELSE '`)
io.WriteString(w, c.req.role)
io.WriteString(w, `' END) FROM (`)
}
io.WriteString(w, conf.RolesQuery)
io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) ELSE '`)
if len(c.req.role) == 0 {
io.WriteString(w, `anon`)
} else {
io.WriteString(w, c.req.role)
}
io.WriteString(w, `' END) FROM (VALUES (1)) AS "_sg_auth_filler") AS "_sg_auth_info"(role) LIMIT 1; `)
} }
stmts[0].sql = w.String() io.WriteString(w, ` ELSE 'user' END) FROM (`)
stmts[0].role = nil io.WriteString(w, conf.RolesQuery)
io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) `)
io.WriteString(w, `ELSE 'anon' END) FROM (VALUES (1)) AS "_sg_auth_filler") AS "_sg_auth_info"(role) LIMIT 1; `)
return stmts, nil return w.String(), nil
}
func hasTablesWithConfig(qc *qcode.QCode, role *configRole) bool {
for _, id := range qc.Roots {
t, err := schema.GetTable(qc.Selects[id].Table)
if err != nil {
return false
}
if _, ok := role.tablesMap[t.Name]; !ok {
return false
}
}
return true
} }

197
serv/core_remote.go Normal file
View File

@ -0,0 +1,197 @@
package serv
import (
"bytes"
"errors"
"fmt"
"net/http"
"sync"
"github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/jsn"
"github.com/dosco/super-graph/qcode"
)
func execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([]byte, error) {
var err error
if len(data) == 0 || st.skipped == 0 {
return data, nil
}
sel := st.qc.Selects
h := xxhash.New()
// fetch the field name used within the db response json
// that are used to mark insertion points and the mapping between
// those field names and their select objects
fids, sfmap := parentFieldIds(h, sel, st.skipped)
// fetch the field values of the marked insertion points
// these values contain the id to be used with fetching remote data
from := jsn.Get(data, fids)
var to []jsn.Field
switch {
case len(from) == 1:
to, err = resolveRemote(hdr, h, from[0], sel, sfmap)
case len(from) > 1:
to, err = resolveRemotes(hdr, h, from, sel, sfmap)
default:
return nil, errors.New("something wrong no remote ids found in db response")
}
if err != nil {
return nil, err
}
var ob bytes.Buffer
err = jsn.Replace(&ob, data, from, to)
if err != nil {
return nil, err
}
return ob.Bytes(), nil
}
func resolveRemote(
hdr http.Header,
h *xxhash.Digest,
field jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
// replacement data for the marked insertion points
// key and value will be replaced by whats below
toA := [1]jsn.Field{}
to := toA[:1]
// use the json key to find the related Select object
k1 := xxhash.Sum64(field.Key)
s, ok := sfmap[k1]
if !ok {
return nil, nil
}
p := sel[s.ParentID]
// then use the Table nme in the Select and it's parent
// to find the resolver to use for this relationship
k2 := mkkey(h, s.Table, p.Table)
r, ok := rmap[k2]
if !ok {
return nil, nil
}
id := jsn.Value(field.Value)
if len(id) == 0 {
return nil, nil
}
//st := time.Now()
b, err := r.Fn(hdr, id)
if err != nil {
return nil, err
}
if len(r.Path) != 0 {
b = jsn.Strip(b, r.Path)
}
var ob bytes.Buffer
if len(s.Cols) != 0 {
err = jsn.Filter(&ob, b, colsToList(s.Cols))
if err != nil {
return nil, err
}
} else {
ob.WriteString("null")
}
to[0] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
return to, nil
}
func resolveRemotes(
hdr http.Header,
h *xxhash.Digest,
from []jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
// replacement data for the marked insertion points
// key and value will be replaced by whats below
to := make([]jsn.Field, len(from))
var wg sync.WaitGroup
wg.Add(len(from))
var cerr error
for i, id := range from {
// use the json key to find the related Select object
k1 := xxhash.Sum64(id.Key)
s, ok := sfmap[k1]
if !ok {
return nil, nil
}
p := sel[s.ParentID]
// then use the Table nme in the Select and it's parent
// to find the resolver to use for this relationship
k2 := mkkey(h, s.Table, p.Table)
r, ok := rmap[k2]
if !ok {
return nil, nil
}
id := jsn.Value(id.Value)
if len(id) == 0 {
return nil, nil
}
go func(n int, id []byte, s *qcode.Select) {
defer wg.Done()
//st := time.Now()
b, err := r.Fn(hdr, id)
if err != nil {
cerr = fmt.Errorf("%s: %s", s.Table, err)
return
}
if len(r.Path) != 0 {
b = jsn.Strip(b, r.Path)
}
var ob bytes.Buffer
if len(s.Cols) != 0 {
err = jsn.Filter(&ob, b, colsToList(s.Cols))
if err != nil {
cerr = fmt.Errorf("%s: %s", s.Table, err)
return
}
} else {
ob.WriteString("null")
}
to[n] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
}(i, id, s)
}
wg.Wait()
return to, cerr
}

View File

@ -4,7 +4,7 @@ package serv
func Fuzz(data []byte) int { func Fuzz(data []byte) int {
gql := string(data) gql := string(data)
isMutation(gql) gqlName(gql)
gqlHash(gql, nil, "") gqlHash(gql, nil, "")
return 1 return 1

View File

@ -10,7 +10,7 @@ func TestFuzzCrashers(t *testing.T) {
} }
for _, f := range crashers { for _, f := range crashers {
isMutation(f) _ = gqlName(f)
gqlHash(f, nil, "") gqlHash(f, nil, "")
} }
} }

View File

@ -8,8 +8,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/gorilla/websocket"
) )
const ( const (
@ -20,8 +18,6 @@ const (
) )
var ( var (
upgrader = websocket.Upgrader{}
errNoUserID = errors.New("no user_id available")
errUnauthorized = errors.New("not authorized") errUnauthorized = errors.New("not authorized")
) )
@ -34,8 +30,6 @@ type gqlReq struct {
hdr http.Header hdr http.Header
} }
type variables map[string]json.RawMessage
type gqlResp struct { type gqlResp struct {
Error string `json:"message,omitempty"` Error string `json:"message,omitempty"`
Data json.RawMessage `json:"data,omitempty"` Data json.RawMessage `json:"data,omitempty"`
@ -70,25 +64,24 @@ type resolver struct {
func apiv1Http(w http.ResponseWriter, r *http.Request) { func apiv1Http(w http.ResponseWriter, r *http.Request) {
ctx := &coreContext{Context: r.Context()} ctx := &coreContext{Context: r.Context()}
if conf.AuthFailBlock && authCheck(ctx) == false { //nolint: errcheck
if conf.AuthFailBlock && !authCheck(ctx) {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(gqlResp{Error: errUnauthorized.Error()}) json.NewEncoder(w).Encode(gqlResp{Error: errUnauthorized.Error()})
return return
} }
b, err := ioutil.ReadAll(io.LimitReader(r.Body, maxReadBytes)) b, err := ioutil.ReadAll(io.LimitReader(r.Body, maxReadBytes))
defer r.Body.Close()
if err != nil { if err != nil {
logger.Err(err).Msg("failed to read request body") errlog.Error().Err(err).Msg("failed to read request body")
errorResp(w, err) errorResp(w, err)
return return
} }
defer r.Body.Close()
err = json.Unmarshal(b, &ctx.req) err = json.Unmarshal(b, &ctx.req)
if err != nil { if err != nil {
logger.Err(err).Msg("failed to decode json request body") errlog.Error().Err(err).Msg("failed to decode json request body")
errorResp(w, err) errorResp(w, err)
return return
} }
@ -100,6 +93,7 @@ func apiv1Http(w http.ResponseWriter, r *http.Request) {
err = ctx.handleReq(w, r) err = ctx.handleReq(w, r)
//nolint: errcheck
if err == errUnauthorized { if err == errUnauthorized {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(gqlResp{Error: err.Error()}) json.NewEncoder(w).Encode(gqlResp{Error: err.Error()})
@ -107,12 +101,13 @@ func apiv1Http(w http.ResponseWriter, r *http.Request) {
} }
if err != nil { if err != nil {
logger.Err(err).Msg("failed to handle request") errlog.Error().Err(err).Msg("failed to handle request")
errorResp(w, err) errorResp(w, err)
return
} }
} }
//nolint: errcheck
func errorResp(w http.ResponseWriter, err error) { func errorResp(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(gqlResp{Error: err.Error()}) json.NewEncoder(w).Encode(gqlResp{Error: err.Error()})
} }

View File

@ -2,6 +2,7 @@ package serv
import "net/http" import "net/http"
//nolint: errcheck
func introspect(w http.ResponseWriter) { func introspect(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{ w.Write([]byte(`{

View File

@ -3,20 +3,19 @@ package serv
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
"github.com/jackc/pgconn" "github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
) )
type preparedItem struct { type preparedItem struct {
stmt *pgconn.StatementDescription sd *pgconn.StatementDescription
args [][]byte args [][]byte
skipped uint32 st *stmt
qc *qcode.QCode
} }
var ( var (
@ -24,83 +23,126 @@ var (
) )
func initPreparedList() { func initPreparedList() {
c := context.Background()
_preparedList = make(map[string]*preparedItem) _preparedList = make(map[string]*preparedItem)
if err := prepareRoleStmt(); err != nil { tx, err := db.Begin(c)
logger.Fatal().Err(err).Msg("failed to prepare get role statement") if err != nil {
errlog.Fatal().Err(err).Send()
} }
defer tx.Rollback(c) //nolint: errcheck
err = prepareRoleStmt(c, tx)
if err != nil {
errlog.Fatal().Err(err).Msg("failed to prepare get role statement")
}
if err := tx.Commit(c); err != nil {
errlog.Fatal().Err(err).Send()
}
success := 0
for _, v := range _allowList.list { for _, v := range _allowList.list {
if len(v.gql) == 0 {
err := prepareStmt(v.gql, v.vars)
if err != nil {
logger.Warn().Str("gql", v.gql).Err(err).Send()
}
}
}
func prepareStmt(gql string, varBytes json.RawMessage) error {
if len(gql) == 0 {
return nil
}
c := &coreContext{Context: context.Background()}
c.req.Query = gql
c.req.Vars = varBytes
stmts, err := c.buildStmt()
if err != nil {
return err
}
if len(stmts) != 0 && stmts[0].qc.Type == qcode.QTQuery {
c.req.Vars = nil
}
for _, s := range stmts {
if len(s.sql) == 0 {
continue continue
} }
finalSQL, am := processTemplate(s.sql) err := prepareStmt(c, v.gql, v.vars)
if err == nil {
ctx := context.Background() success++
continue
tx, err := db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
pstmt, err := tx.Prepare(ctx, "", finalSQL)
if err != nil {
return err
} }
var key string if len(v.vars) == 0 {
logger.Warn().Err(err).Msg(v.gql)
if s.role == nil {
key = gqlHash(gql, c.req.Vars, "")
} else { } else {
key = gqlHash(gql, c.req.Vars, s.role.Name) logger.Warn().Err(err).Msgf("%s %s", v.vars, v.gql)
} }
}
_preparedList[key] = &preparedItem{ logger.Info().
stmt: pstmt, Msgf("Registered %d of %d queries from allow.list as prepared statements",
args: am, success, len(_allowList.list))
skipped: s.skipped, }
qc: s.qc,
}
if err := tx.Commit(ctx); err != nil { func prepareStmt(c context.Context, gql string, vars []byte) error {
qt := qcode.GetQType(gql)
q := []byte(gql)
tx, err := db.Begin(c)
if err != nil {
return err
}
defer tx.Rollback(c) //nolint: errcheck
switch qt {
case qcode.QTQuery:
stmts1, err := buildMultiStmt(q, vars)
if err != nil {
return err return err
} }
err = prepare(c, tx, &stmts1[0], gqlHash(gql, vars, "user"))
if err != nil {
return err
}
stmts2, err := buildRoleStmt(q, vars, "anon")
if err != nil {
return err
}
err = prepare(c, tx, &stmts2[0], gqlHash(gql, vars, "anon"))
if err != nil {
return err
}
case qcode.QTMutation:
for _, role := range conf.Roles {
stmts, err := buildRoleStmt(q, vars, role.Name)
if err != nil {
return err
}
err = prepare(c, tx, &stmts[0], gqlHash(gql, vars, role.Name))
if err != nil {
return err
}
}
}
if len(vars) == 0 {
logger.Debug().Msgf("Building prepared statement for:\n %s", gql)
} else {
logger.Debug().Msgf("Building prepared statement:\n %s\n%s", vars, gql)
}
if err := tx.Commit(c); err != nil {
return err
} }
return nil return nil
} }
func prepareRoleStmt() error { func prepare(c context.Context, tx pgx.Tx, st *stmt, key string) error {
finalSQL, am := processTemplate(st.sql)
sd, err := tx.Prepare(c, "", finalSQL)
if err != nil {
return err
}
_preparedList[key] = &preparedItem{
sd: sd,
args: am,
st: st,
}
return nil
}
// nolint: errcheck
func prepareRoleStmt(c context.Context, tx pgx.Tx) error {
if len(conf.RolesQuery) == 0 { if len(conf.RolesQuery) == 0 {
return nil return nil
} }
@ -125,15 +167,7 @@ func prepareRoleStmt() error {
roleSQL, _ := processTemplate(w.String()) roleSQL, _ := processTemplate(w.String())
ctx := context.Background() _, err := tx.Prepare(c, "_sg_get_role", roleSQL)
tx, err := db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
_, err = tx.Prepare(ctx, "_sg_get_role", roleSQL)
if err != nil { if err != nil {
return err return err
} }
@ -142,19 +176,31 @@ func prepareRoleStmt() error {
} }
func processTemplate(tmpl string) (string, [][]byte) { func processTemplate(tmpl string) (string, [][]byte) {
t := fasttemplate.New(tmpl, `{{`, `}}`) st := struct {
am := make([][]byte, 0, 5) vmap map[string]int
i := 0 am [][]byte
i int
}{
vmap: make(map[string]int),
am: make([][]byte, 0, 5),
i: 0,
}
vmap := make(map[string]int) execFunc := func(w io.Writer, tag string) (int, error) {
if n, ok := st.vmap[tag]; ok {
return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
if n, ok := vmap[tag]; ok {
return w.Write([]byte(fmt.Sprintf("$%d", n))) return w.Write([]byte(fmt.Sprintf("$%d", n)))
} }
am = append(am, []byte(tag)) st.am = append(st.am, []byte(tag))
i++ st.i++
vmap[tag] = i st.vmap[tag] = st.i
return w.Write([]byte(fmt.Sprintf("$%d", i))) return w.Write([]byte(fmt.Sprintf("$%d", st.i)))
}), am }
t1 := fasttemplate.New(tmpl, `'{{`, `}}'`)
ts1 := t1.ExecuteFuncString(execFunc)
t2 := fasttemplate.New(ts1, `{{`, `}}`)
ts2 := t2.ExecuteFuncString(execFunc)
return ts2, st.am
} }

View File

@ -108,7 +108,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
// Ensure that we use the correct events, as they are not uniform across // Ensure that we use the correct events, as they are not uniform across
// platforms. See https://github.com/fsnotify/fsnotify/issues/74 // platforms. See https://github.com/fsnotify/fsnotify/issues/74
if conf.Production == false && strings.HasSuffix(event.Name, "/allow.list") { if !conf.Production && strings.HasSuffix(event.Name, "/allow.list") {
continue continue
} }
@ -168,7 +168,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
func ReExec() { func ReExec() {
err := syscall.Exec(binSelf, append([]string{binSelf}, os.Args[1:]...), os.Environ()) err := syscall.Exec(binSelf, append([]string{binSelf}, os.Args[1:]...), os.Environ())
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("cannot restart") errlog.Fatal().Err(err).Msg("cannot restart")
} }
} }

View File

@ -117,7 +117,7 @@ func buildFn(r configRemote) func(http.Header, []byte) ([]byte, error) {
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
logger.Error().Err(err).Msgf("Failed to connect to: %s", uri) errlog.Error().Err(err).Msgf("Failed to connect to: %s", uri)
return nil, err return nil, err
} }
defer res.Body.Close() defer res.Body.Close()

345
serv/rice-box.go Normal file

File diff suppressed because one or more lines are too long

View File

@ -15,13 +15,15 @@ import (
) )
func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) { func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
schema, err := psql.NewDBSchema(db, c.getAliasMap()) var err error
schema, err = psql.NewDBSchema(db, c.getAliasMap())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
conf := qcode.Config{ conf := qcode.Config{
Blocklist: c.DB.Defaults.Blocklist, Blocklist: c.DB.Blocklist,
KeepArgs: false, KeepArgs: false,
} }
@ -74,12 +76,16 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
delete.Filters = blockFilter delete.Filters = blockFilter
} }
qc.AddRole(r.Name, t.Name, qcode.TRConfig{ err := qc.AddRole(r.Name, t.Name, qcode.TRConfig{
Query: query, Query: query,
Insert: insert, Insert: insert,
Update: update, Update: update,
Delete: delete, Delete: delete,
}) })
if err != nil {
return nil, nil, err
}
} }
} }
@ -92,7 +98,7 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
} }
func initWatcher(cpath string) { func initWatcher(cpath string) {
if conf.WatchAndReload == false { if !conf.WatchAndReload {
return return
} }
@ -106,7 +112,7 @@ func initWatcher(cpath string) {
go func() { go func() {
err := Do(logger.Printf, d) err := Do(logger.Printf, d)
if err != nil { if err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
}() }()
} }
@ -139,7 +145,7 @@ func startHTTP() {
<-sigint <-sigint
if err := srv.Shutdown(context.Background()); err != nil { if err := srv.Shutdown(context.Background()); err != nil {
logger.Error().Err(err).Msg("shutdown signal received") errlog.Error().Err(err).Msg("shutdown signal received")
} }
close(idleConnsClosed) close(idleConnsClosed)
}() }()
@ -148,18 +154,16 @@ func startHTTP() {
db.Close() db.Close()
}) })
var ident string logger.Info().
Str("version", version).
if len(conf.AppName) == 0 { Str("git_branch", gitBranch).
ident = conf.Env Str("host_post", hostPort).
} else { Str("app_name", conf.AppName).
ident = conf.AppName Str("env", conf.Env).
} Msgf("%s listening", serverName)
fmt.Printf("%s listening on %s (%s)\n", serverName, hostPort, ident)
if err := srv.ListenAndServe(); err != http.ErrServerClosed { if err := srv.ListenAndServe(); err != http.ErrServerClosed {
logger.Error().Err(err).Msg("server closed") errlog.Error().Err(err).Msg("server closed")
} }
<-idleConnsClosed <-idleConnsClosed
@ -169,6 +173,7 @@ func routeHandler() http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/api/v1/graphql", withAuth(apiv1Http)) mux.Handle("/api/v1/graphql", withAuth(apiv1Http))
if conf.WebUI { if conf.WebUI {
mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox())) mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox()))
} }

View File

@ -28,9 +28,7 @@ func (pl *Logger) Log(ctx context.Context, level pgx.LogLevel, msg string, data
zlevel = zerolog.ErrorLevel zlevel = zerolog.ErrorLevel
case pgx.LogLevelWarn: case pgx.LogLevelWarn:
zlevel = zerolog.WarnLevel zlevel = zerolog.WarnLevel
case pgx.LogLevelInfo: case pgx.LogLevelDebug, pgx.LogLevelInfo:
zlevel = zerolog.InfoLevel
case pgx.LogLevelDebug:
zlevel = zerolog.DebugLevel zlevel = zerolog.DebugLevel
default: default:
zlevel = zerolog.DebugLevel zlevel = zerolog.DebugLevel

View File

@ -12,6 +12,7 @@ import (
"github.com/dosco/super-graph/jsn" "github.com/dosco/super-graph/jsn"
) )
// nolint: errcheck
func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 { func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 {
h.WriteString(k1) h.WriteString(k1)
h.WriteString(k2) h.WriteString(k2)
@ -21,6 +22,7 @@ func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 {
return v return v
} }
// nolint: errcheck
func gqlHash(b string, vars []byte, role string) string { func gqlHash(b string, vars []byte, role string) string {
b = strings.TrimSpace(b) b = strings.TrimSpace(b)
h := sha1.New() h := sha1.New()
@ -64,7 +66,7 @@ func gqlHash(b string, vars []byte, role string) string {
} else { } else {
starting = false starting = false
s = e s = e
for e < len(b) && ws(b[e]) == false { for e < len(b) && !ws(b[e]) {
e++ e++
} }
if e != 0 { if e != 0 {
@ -81,7 +83,7 @@ func gqlHash(b string, vars []byte, role string) string {
io.WriteString(h, role) io.WriteString(h, role)
} }
if vars == nil || len(vars) == 0 { if len(vars) == 0 {
return hex.EncodeToString(h.Sum(nil)) return hex.EncodeToString(h.Sum(nil))
} }
@ -106,17 +108,28 @@ func al(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
} }
func isMutation(sql string) bool { func gqlName(b string) string {
for i := range sql { state, s := 0, 0
b := sql[i]
if b == '{' { for i := 0; i < len(b); i++ {
return false switch {
} case state == 2 && b[i] == '{':
if al(b) { return b[s:i]
return (b == 'm' || b == 'M') case state == 2 && b[i] == ' ':
return b[s:i]
case state == 1 && b[i] == '{':
return ""
case state == 1 && b[i] != ' ':
s = i
state = 2
case state == 1 && b[i] == ' ':
continue
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
state = 1
} }
} }
return false
return ""
} }
func findStmt(role string, stmts []stmt) *stmt { func findStmt(role string, stmts []stmt) *stmt {

View File

@ -229,3 +229,80 @@ func TestGQLHashWithVars2(t *testing.T) {
t.Fatal("Hashes don't match they should") t.Fatal("Hashes don't match they should")
} }
} }
func TestGQLName1(t *testing.T) {
var q = `
query {
products(
distinct: [price]
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
) { id name } }`
name := gqlName(q)
if len(name) != 0 {
t.Fatal("Name should be empty, not ", name)
}
}
func TestGQLName2(t *testing.T) {
var q = `
query hakuna_matata {
products(
distinct: [price]
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
) {
id
name
}
}`
name := gqlName(q)
if name != "hakuna_matata" {
t.Fatal("Name should be 'hakuna_matata', not ", name)
}
}
func TestGQLName3(t *testing.T) {
var q = `
mutation means{ users { id } }`
// var v2 = ` { products( limit: 30, order_by: { price: desc }, distinct: [ price ] where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) { id name price user { id email } } } `
name := gqlName(q)
if name != "means" {
t.Fatal("Name should be 'means', not ", name)
}
}
func TestGQLName4(t *testing.T) {
var q = `
query no_worries
users {
id
}
}`
name := gqlName(q)
if name != "no_worries" {
t.Fatal("Name should be 'no_worries', not ", name)
}
}
func TestGQLName5(t *testing.T) {
var q = `
{
users {
id
}
}`
name := gqlName(q)
if len(name) != 0 {
t.Fatal("Name should be empty, not ", name)
}
}

View File

@ -77,7 +77,7 @@ SQL Output
database: database:
variables: variables:
account_id: "select account_id from users where id = $user_id" admin_account_id: "5"
defaults: defaults:
Filters: ["{ user_id: { eq: $user_id } }"] Filters: ["{ user_id: { eq: $user_id } }"]

View File

@ -3,7 +3,7 @@ host_port: 0.0.0.0:8080
web_ui: true web_ui: true
# debug, info, warn, error, fatal, panic # debug, info, warn, error, fatal, panic
log_level: "debug" log_level: "info"
# When production mode is 'true' only queries # When production mode is 'true' only queries
# from the allow list are permitted. # from the allow list are permitted.
@ -97,23 +97,18 @@ database:
# Enable this if you need the user id in triggers, etc # Enable this if you need the user id in triggers, etc
set_user_id: false set_user_id: false
# Define variables here that you want to use in filters # Define additional variables here to be used with filters
# sub-queries must be wrapped in ()
variables: variables:
account_id: "(select account_id from users where id = $user_id)" admin_account_id: "5"
# Define defaults to for the field key and values below # Field and table names that you wish to block
defaults: blocklist:
# filters: ["{ user_id: { eq: $user_id } }"] - ar_internal_metadata
- schema_migrations
# Field and table names that you wish to block - secret
blocklist: - password
- ar_internal_metadata - encrypted
- schema_migrations - token
- secret
- password
- encrypted
- token
tables: tables:
- name: customers - name: customers
@ -185,11 +180,10 @@ roles:
- updated_at: "now" - updated_at: "now"
delete: delete:
deny: true block: true
- name: admin - name: admin
match: id = 1 match: id = 1000
tables: tables:
- name: users - name: users
# query: filters: []
# filters: ["{ account_id: { _eq: $account_id } }"]

View File

@ -1,4 +1,4 @@
version: '3' version: '3.4'
services: services:
db: db:
image: postgres image: postgres

View File

@ -7,7 +7,7 @@ host_port: 0.0.0.0:8080
web_ui: false web_ui: false
# debug, info, warn, error, fatal, panic, disable # debug, info, warn, error, fatal, panic, disable
log_level: "info" log_level: "warn"
# When production mode is 'true' only queries # When production mode is 'true' only queries
# from the allow list are permitted. # from the allow list are permitted.
# When it's 'false' all queries are saved to the # When it's 'false' all queries are saved to the
@ -40,40 +40,6 @@ enable_tracing: true
# SG_AUTH_RAILS_REDIS_PASSWORD # SG_AUTH_RAILS_REDIS_PASSWORD
# SG_AUTH_JWT_PUBLIC_KEY_FILE # SG_AUTH_JWT_PUBLIC_KEY_FILE
# inflections:
# person: people
# sheep: sheep
auth:
# Can be 'rails' or 'jwt'
type: rails
cookie: _{% app_name_slug %}_session
rails:
# Rails version this is used for reading the
# various cookies formats.
version: 5.2
# Found in 'Rails.application.config.secret_key_base'
secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566
# Remote cookie store. (memcache or redis)
# url: redis://127.0.0.1:6379
# password: test
# max_idle: 80,
# max_active: 12000,
# In most cases you don't need these
# salt: "encrypted cookie"
# sign_salt: "signed encrypted cookie"
# auth_salt: "authenticated encrypted cookie"
# jwt:
# provider: auth0
# secret: abc335bfcfdb04e50db5bb0a4d67ab9
# public_key_file: /secrets/public_key.pem
# public_key_type: ecdsa #rsa
database: database:
type: postgres type: postgres
host: db host: db