Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
f518d5fc69 | |||
7583326d21 | |||
8024a8420b | |||
6bd27d7c79 | |||
a4c09dedd5 | |||
176514b5f1 | |||
396f4bcfd8 | |||
51c5f037f4 | |||
c576f1ae16 | |||
0a6995eaca | |||
06ed823a51 | |||
3438c3efb9 | |||
605ec63466 |
@ -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,23 @@ 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 {
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"name": "Gumbo1",
|
|
||||||
"created_at": "now",
|
|
||||||
"updated_at": "now"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Gumbo2",
|
|
||||||
"created_at": "now",
|
|
||||||
"updated_at": "now"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
query {
|
query {
|
||||||
products {
|
products {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
description
|
|
||||||
users {
|
|
||||||
email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query {
|
|
||||||
users {
|
|
||||||
id
|
|
||||||
email
|
|
||||||
picture: avatar
|
|
||||||
password
|
|
||||||
full_name
|
|
||||||
products(limit: 2, where: {price: {gt: 10}}) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
price
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,11 +5,11 @@ web_ui: true
|
|||||||
# debug, info, warn, error, fatal, panic
|
# debug, info, warn, error, fatal, panic
|
||||||
log_level: "debug"
|
log_level: "debug"
|
||||||
|
|
||||||
# Disable this in development to get a list of
|
# When production mode is 'true' only queries
|
||||||
# queries used. When enabled super graph
|
# from the allow list are permitted.
|
||||||
# will only allow queries from this list
|
# When it's 'false' all queries are saved to the
|
||||||
# List saved to ./config/allow.list
|
# the allow list in ./config/allow.list
|
||||||
use_allow_list: false
|
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 } }"]
|
|
||||||
|
@ -9,11 +9,11 @@ web_ui: false
|
|||||||
# debug, info, warn, error, fatal, panic, disable
|
# debug, info, warn, error, fatal, panic, disable
|
||||||
log_level: "info"
|
log_level: "info"
|
||||||
|
|
||||||
# Disable this in development to get a list of
|
# When production mode is 'true' only queries
|
||||||
# queries used. When enabled super graph
|
# from the allow list are permitted.
|
||||||
# will only allow queries from this list
|
# When it's 'false' all queries are saved to the
|
||||||
# List saved to ./config/allow.list
|
# the allow list in ./config/allow.list
|
||||||
use_allow_list: true
|
production: true
|
||||||
|
|
||||||
# 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: true
|
auth_fail_block: true
|
||||||
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,12 @@
|
|||||||
:item="actionLink"
|
:item="actionLink"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="px-4 py-3 my-8 border-2 border-gray-500 text-gray-600 font-bold rounded"
|
||||||
|
href="https://github.com/dosco/super-graph"
|
||||||
|
target="_blank"
|
||||||
|
>Github</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
|
let ogprefix = 'og: http://ogp.me/ns#'
|
||||||
|
let title = 'Super Graph'
|
||||||
|
let description = 'An instant GraphQL API for your app. No code needed.'
|
||||||
|
let color = '#f42525'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
title: 'Super Graph',
|
title: title,
|
||||||
description: 'Get an instant GraphQL API for your Rails apps.',
|
description: description,
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
logo: '/hologram.svg',
|
logo: '/hologram.svg',
|
||||||
@ -15,6 +20,22 @@ module.exports = {
|
|||||||
serviceWorker: {
|
serviceWorker: {
|
||||||
updatePopup: true
|
updatePopup: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
head: [
|
||||||
|
//['link', { rel: 'icon', href: `/assets/favicon.ico` }],
|
||||||
|
['meta', { prefix: ogprefix, property: 'og:title', content: title }],
|
||||||
|
['meta', { prefix: ogprefix, property: 'twitter:title', content: title }],
|
||||||
|
['meta', { prefix: ogprefix, property: 'og:type', content: 'website' }],
|
||||||
|
['meta', { prefix: ogprefix, property: 'og:url', content: 'https://supergraph.dev' }],
|
||||||
|
['meta', { prefix: ogprefix, property: 'og:description', content: description }],
|
||||||
|
//['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-status-bar-style', content: 'black' }],
|
||||||
|
// ['link', { rel: 'apple-touch-icon', href: `/assets/apple-touch-icon.png` }],
|
||||||
|
// ['link', { rel: 'mask-icon', href: '/assets/safari-pinned-tab.svg', color: color }],
|
||||||
|
// ['meta', { name: 'msapplication-TileImage', content: '/assets/mstile-150x150.png' }],
|
||||||
|
// ['meta', { name: 'msapplication-TileColor', content: color }],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
postcss: {
|
postcss: {
|
||||||
|
112
docs/guide.md
112
docs/guide.md
@ -276,7 +276,7 @@ transmission_gear_type
|
|||||||
// Text
|
// Text
|
||||||
word
|
word
|
||||||
sentence
|
sentence
|
||||||
paragrph
|
paragraph
|
||||||
question
|
question
|
||||||
quote
|
quote
|
||||||
|
|
||||||
@ -476,6 +476,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 +923,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 +1083,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.
|
||||||
@ -1149,11 +1179,11 @@ web_ui: true
|
|||||||
# debug, info, warn, error, fatal, panic
|
# debug, info, warn, error, fatal, panic
|
||||||
log_level: "debug"
|
log_level: "debug"
|
||||||
|
|
||||||
# Disable this in development to get a list of
|
# When production mode is 'true' only queries
|
||||||
# queries used. When enabled super graph
|
# from the allow list are permitted.
|
||||||
# will only allow queries from this list
|
# When it's 'false' all queries are saved to the
|
||||||
# List saved to ./config/allow.list
|
# the allow list in ./config/allow.list
|
||||||
use_allow_list: false
|
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
|
||||||
@ -1241,23 +1271,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 +1354,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 } }"]
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -374,10 +374,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))
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var migrationPattern = regexp.MustCompile(`\A(\d+)_.+\.sql\z`)
|
var migrationPattern = regexp.MustCompile(`\A(\d+)_[^\.]+\.sql\z`)
|
||||||
|
|
||||||
var ErrNoFwMigration = errors.Errorf("no sql in forward migration step")
|
var ErrNoFwMigration = errors.Errorf("no sql in forward migration step")
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ func FindMigrationsEx(path string, fs MigratorFS) ([]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mcount := len(paths) + 100
|
mcount := len(paths)
|
||||||
|
|
||||||
if n < int64(mcount) {
|
if n < int64(mcount) {
|
||||||
return nil, fmt.Errorf("Duplicate migration %d", n)
|
return nil, fmt.Errorf("Duplicate migration %d", n)
|
||||||
|
@ -77,9 +77,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)
|
||||||
@ -137,16 +137,23 @@ func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w io.Writer
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range root.PresetList {
|
for i := range root.PresetList {
|
||||||
|
cn := root.PresetList[i]
|
||||||
|
col, ok := ti.Columns[cn]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
io.WriteString(c.w, `, `)
|
io.WriteString(c.w, `, `)
|
||||||
}
|
}
|
||||||
if values {
|
if values {
|
||||||
io.WriteString(c.w, `'`)
|
io.WriteString(c.w, `'`)
|
||||||
io.WriteString(c.w, root.PresetMap[root.PresetList[i]])
|
io.WriteString(c.w, root.PresetMap[cn])
|
||||||
io.WriteString(c.w, `'`)
|
io.WriteString(c.w, `' :: `)
|
||||||
|
io.WriteString(c.w, col.Type)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
io.WriteString(c.w, `"`)
|
io.WriteString(c.w, `"`)
|
||||||
io.WriteString(c.w, root.PresetList[i])
|
io.WriteString(c.w, cn)
|
||||||
io.WriteString(c.w, `"`)
|
io.WriteString(c.w, `"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,9 +174,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)
|
||||||
|
@ -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', 'now', '$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") 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' 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}`),
|
||||||
|
@ -171,7 +171,7 @@ func TestMain(m *testing.M) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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{
|
||||||
|
178
psql/query.go
178
psql/query.go
@ -77,30 +77,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 +133,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 +177,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
|
||||||
}
|
}
|
||||||
@ -191,15 +224,15 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
|
|||||||
fallthrough
|
fallthrough
|
||||||
case RelBelongTo:
|
case RelBelongTo:
|
||||||
if _, ok := colmap[rel.Col2]; !ok {
|
if _, ok := colmap[rel.Col2]; !ok {
|
||||||
cols = append(cols, &qcode.Column{ti.Name, rel.Col2, rel.Col2})
|
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Col2, FieldName: rel.Col2})
|
||||||
}
|
}
|
||||||
case RelOneToManyThrough:
|
case RelOneToManyThrough:
|
||||||
if _, ok := colmap[rel.Col1]; !ok {
|
if _, ok := colmap[rel.Col1]; !ok {
|
||||||
cols = append(cols, &qcode.Column{ti.Name, rel.Col1, rel.Col1})
|
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Col1, FieldName: rel.Col1})
|
||||||
}
|
}
|
||||||
case RelRemote:
|
case RelRemote:
|
||||||
if _, ok := colmap[rel.Col1]; !ok {
|
if _, ok := colmap[rel.Col1]; !ok {
|
||||||
cols = append(cols, &qcode.Column{ti.Name, rel.Col1, rel.Col2})
|
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Col1, FieldName: rel.Col2})
|
||||||
}
|
}
|
||||||
skipped |= (1 << uint(id))
|
skipped |= (1 << uint(id))
|
||||||
|
|
||||||
@ -219,7 +252,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
|||||||
if ti.Singular == false {
|
if ti.Singular == false {
|
||||||
//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 +265,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 +278,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 +293,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 +328,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 +337,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`)
|
||||||
@ -319,9 +352,9 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ti.Singular == false {
|
if ti.Singular == false {
|
||||||
//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
|
||||||
@ -340,9 +373,9 @@ func (c *compilerContext) renderLateralJoinClose(sel *qcode.Select) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderJoin(sel *qcode.Select) error {
|
func (c *compilerContext) renderJoin(sel *qcode.Select, ti *DBTableInfo) error {
|
||||||
parent := &c.s[sel.ParentID]
|
parent := &c.s[sel.ParentID]
|
||||||
return c.renderJoinByName(sel.Table, parent.Table, parent.ID)
|
return c.renderJoinByName(ti.Name, parent.Table, parent.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderJoinByName(table, parent string, id int32) error {
|
func (c *compilerContext) renderJoinByName(table, parent string, id int32) error {
|
||||||
@ -445,24 +478,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 +499,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
|
||||||
|
|
||||||
@ -607,7 +634,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !isRoot {
|
if !isRoot {
|
||||||
if err := c.renderJoin(sel); err != nil {
|
if err := c.renderJoin(sel, ti); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,8 +668,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 +677,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`)
|
||||||
@ -691,7 +718,7 @@ func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInf
|
|||||||
|
|
||||||
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
||||||
parent := c.s[sel.ParentID]
|
parent := c.s[sel.ParentID]
|
||||||
return c.renderRelationshipByName(sel.Table, parent.Table, parent.ID)
|
return c.renderRelationshipByName(ti.Name, parent.Table, parent.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compilerContext) renderRelationshipByName(table, parent string, id int32) error {
|
func (c *compilerContext) renderRelationshipByName(table, parent string, id int32) error {
|
||||||
@ -817,7 +844,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 +877,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 +944,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 +956,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 col, 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 +974,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 +1051,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 +1068,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 +1077,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, `)`)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
2
qcode/cleanup.sh
Executable file
2
qcode/cleanup.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
cd corpus && rm -rf $(find . ! -name '00?.gql')
|
@ -1,6 +1,7 @@
|
|||||||
package qcode
|
package qcode
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -121,3 +122,12 @@ func mapToList(m map[string]string) []string {
|
|||||||
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
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -148,24 +148,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)
|
||||||
@ -259,6 +248,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) == false {
|
||||||
|
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) == false {
|
||||||
|
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
|
||||||
@ -300,16 +320,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) {
|
||||||
|
@ -14,10 +14,10 @@ func TestCompile1(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
_, 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)
|
||||||
|
@ -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 {
|
||||||
@ -577,7 +594,7 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) error {
|
|||||||
case nodeVar:
|
case nodeVar:
|
||||||
ex.Type = ValVar
|
ex.Type = ValVar
|
||||||
default:
|
default:
|
||||||
fmt.Errorf("expecting a string, int, float or variable")
|
return fmt.Errorf("expecting a string, int, float or variable")
|
||||||
}
|
}
|
||||||
|
|
||||||
sel.Where = ex
|
sel.Where = ex
|
||||||
|
23
qcode/utils.go
Normal file
23
qcode/utils.go
Normal 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')
|
||||||
|
}
|
@ -46,7 +46,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 +56,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 +66,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.UseAllowList {
|
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 {
|
||||||
@ -187,7 +187,6 @@ func (al *allowList) load() {
|
|||||||
item.gql = q
|
item.gql = q
|
||||||
item.vars = varBytes
|
item.vars = varBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
varBytes = nil
|
varBytes = nil
|
||||||
|
|
||||||
} else if ty == AL_VARS {
|
} else if ty == AL_VARS {
|
||||||
|
80
serv/args.go
80
serv/args.go
@ -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(`'`))
|
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,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 +51,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 +95,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
48
serv/cmd.go
48
serv/cmd.go
@ -22,16 +22,18 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger *zerolog.Logger
|
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 +112,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",
|
||||||
@ -128,19 +137,14 @@ e.g. db:migrate -+1
|
|||||||
"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,7 +163,7 @@ 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"))
|
||||||
}
|
}
|
||||||
@ -176,7 +180,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 +215,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 +250,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 +273,20 @@ 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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -36,6 +36,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"})
|
||||||
|
|
||||||
@ -48,24 +49,30 @@ func cmdDBSetup(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if os.IsNotExist(err) == false {
|
if os.IsNotExist(err) == false {
|
||||||
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 +80,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 +100,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)
|
||||||
@ -110,12 +112,7 @@ func cmdDBNew(cmd *cobra.Command, args []string) {
|
|||||||
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 +121,7 @@ func cmdDBNew(cmd *cobra.Command, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
mname := fmt.Sprintf("%03d_%s.sql", len(m)+100, 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 +141,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()
|
||||||
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 +187,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)
|
||||||
}
|
}
|
||||||
@ -226,17 +218,15 @@ func cmdDBMigrate(cmd *cobra.Command, args []string) {
|
|||||||
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 +241,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
|
||||||
|
@ -134,12 +134,12 @@ func ifNotExists(filePath string, doFn func(string) error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if os.IsNotExist(err) == false {
|
if os.IsNotExist(err) == false {
|
||||||
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)
|
||||||
|
@ -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.UseAllowList = 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,7 +36,7 @@ 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()
|
||||||
@ -50,34 +52,77 @@ 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)
|
||||||
|
|
||||||
|
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
|
||||||
@ -156,10 +201,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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -23,6 +23,7 @@ type config struct {
|
|||||||
LogLevel string `mapstructure:"log_level"`
|
LogLevel string `mapstructure:"log_level"`
|
||||||
EnableTracing bool `mapstructure:"enable_tracing"`
|
EnableTracing bool `mapstructure:"enable_tracing"`
|
||||||
UseAllowList bool `mapstructure:"use_allow_list"`
|
UseAllowList bool `mapstructure:"use_allow_list"`
|
||||||
|
Production bool
|
||||||
WatchAndReload bool `mapstructure:"reload_on_config_change"`
|
WatchAndReload bool `mapstructure:"reload_on_config_change"`
|
||||||
AuthFailBlock bool `mapstructure:"auth_fail_block"`
|
AuthFailBlock bool `mapstructure:"auth_fail_block"`
|
||||||
SeedFile string `mapstructure:"seed_file"`
|
SeedFile string `mapstructure:"seed_file"`
|
||||||
@ -63,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"`
|
||||||
@ -82,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 {
|
||||||
@ -142,9 +139,10 @@ type configRoleTable struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type configRole struct {
|
type configRole struct {
|
||||||
Name string
|
Name string
|
||||||
Match string
|
Match string
|
||||||
Tables []configRoleTable
|
Tables []configRoleTable
|
||||||
|
tablesMap map[string]*configRoleTable
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfig(name string) *viper.Viper {
|
func newConfig(name string) *viper.Viper {
|
||||||
@ -195,6 +193,10 @@ func (c *config) Init(vi *viper.Viper) error {
|
|||||||
c.Tables = c.DB.Tables
|
c.Tables = c.DB.Tables
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.UseAllowList {
|
||||||
|
c.Production = true
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range c.Inflections {
|
for k, v := range c.Inflections {
|
||||||
flect.AddPlural(k, v)
|
flect.AddPlural(k, v)
|
||||||
}
|
}
|
||||||
@ -215,25 +217,32 @@ 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)
|
||||||
rolesMap[role.Name] = struct{}{}
|
role.tablesMap = make(map[string]*configRoleTable)
|
||||||
|
|
||||||
|
for n, table := range role.Tables {
|
||||||
|
role.tablesMap[table.Name] = &role.Tables[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@ -250,7 +259,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{}{}
|
||||||
}
|
}
|
||||||
@ -261,7 +270,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{}{}
|
||||||
}
|
}
|
||||||
|
433
serv/core.go
433
serv/core.go
@ -8,11 +8,9 @@ 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"
|
||||||
@ -32,6 +30,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 +49,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
|
||||||
|
var err error
|
||||||
|
|
||||||
logger.Debug().Str("role", c.req.role).Msg(c.req.Query)
|
if conf.Production {
|
||||||
|
data, st, err = c.resolvePreparedSQL()
|
||||||
if conf.UseAllowList {
|
|
||||||
var ps *preparedItem
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
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 +107,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 +118,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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +212,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 == false {
|
||||||
|
// _allowList.add(&c.req)
|
||||||
|
// }
|
||||||
|
|
||||||
if mutation {
|
if len(stmts) > 1 {
|
||||||
err = tx.QueryRow(c, finalSQL).Scan(&root)
|
if st = findStmt(role, stmts); st == nil {
|
||||||
} else {
|
return nil, nil, fmt.Errorf("invalid role '%s' returned", role)
|
||||||
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.UseAllowList == false {
|
|
||||||
_allowList.add(&c.req)
|
|
||||||
}
|
|
||||||
|
|
||||||
return root, st.skipped, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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{[]byte(s.FieldName), 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{[]byte(s.FieldName), 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 +276,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 +297,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 +305,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--
|
||||||
@ -521,6 +367,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)
|
||||||
}
|
}
|
||||||
|
@ -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,128 +18,171 @@ 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) == false {
|
||||||
|
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 := range conf.Roles {
|
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]
|
||||||
|
|
||||||
if mutation && len(c.req.role) != 0 && role.Name != c.req.role {
|
qc, err := qcompile.Compile(gql, role.Name)
|
||||||
continue
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
|
||||||
if i > 0 {
|
|
||||||
qc, err = qcompile.Compile(gql, role.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
197
serv/core_remote.go
Normal 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
|
||||||
|
}
|
@ -4,7 +4,6 @@ package serv
|
|||||||
|
|
||||||
func Fuzz(data []byte) int {
|
func Fuzz(data []byte) int {
|
||||||
gql := string(data)
|
gql := string(data)
|
||||||
isMutation(gql)
|
|
||||||
gqlHash(gql, nil, "")
|
gqlHash(gql, nil, "")
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
@ -10,7 +10,6 @@ func TestFuzzCrashers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range crashers {
|
for _, f := range crashers {
|
||||||
isMutation(f)
|
|
||||||
gqlHash(f, nil, "")
|
gqlHash(f, nil, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
serv/http.go
13
serv/http.go
@ -21,7 +21,6 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
upgrader = websocket.Upgrader{}
|
upgrader = websocket.Upgrader{}
|
||||||
errNoUserID = errors.New("no user_id available")
|
|
||||||
errUnauthorized = errors.New("not authorized")
|
errUnauthorized = errors.New("not authorized")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -77,18 +76,16 @@ func apiv1Http(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -107,12 +104,12 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()})
|
||||||
}
|
}
|
||||||
|
203
serv/prepare.go
203
serv/prepare.go
@ -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,119 @@ 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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +160,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 +169,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
|
||||||
}
|
}
|
||||||
|
@ -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.UseAllowList == false && strings.HasSuffix(event.Name, "/allow.list") {
|
if conf.Production == false && 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
27
serv/serv.go
27
serv/serv.go
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +108,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 +141,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 +150,14 @@ func startHTTP() {
|
|||||||
db.Close()
|
db.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
var ident string
|
logger.Info().
|
||||||
|
Str("host_post", hostPort).
|
||||||
if len(conf.AppName) == 0 {
|
Str("app_name", conf.AppName).
|
||||||
ident = conf.Env
|
Str("env", conf.Env).
|
||||||
} else {
|
Msgf("%s listening", serverName)
|
||||||
ident = conf.AppName
|
|
||||||
}
|
|
||||||
|
|
||||||
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 +167,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()))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -106,19 +106,6 @@ func al(b byte) bool {
|
|||||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMutation(sql string) bool {
|
|
||||||
for i := range sql {
|
|
||||||
b := sql[i]
|
|
||||||
if b == '{' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if al(b) {
|
|
||||||
return (b == 'm' || b == 'M')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func findStmt(role string, stmts []stmt) *stmt {
|
func findStmt(role string, stmts []stmt) *stmt {
|
||||||
for i := range stmts {
|
for i := range stmts {
|
||||||
if stmts[i].role.Name != role {
|
if stmts[i].role.Name != role {
|
||||||
|
@ -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 } }"]
|
||||||
|
50
tmpl/dev.yml
50
tmpl/dev.yml
@ -1,15 +1,15 @@
|
|||||||
app_name: "{{app_name}} Development"
|
app_name: "{% app_name %} Development"
|
||||||
host_port: 0.0.0.0:8080
|
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"
|
||||||
|
|
||||||
# Disable this in development to get a list of
|
# When production mode is 'true' only queries
|
||||||
# queries used. When enabled super graph
|
# from the allow list are permitted.
|
||||||
# will only allow queries from this list
|
# When it's 'false' all queries are saved to the
|
||||||
# List saved to ./config/allow.list
|
# the allow list in ./config/allow.list
|
||||||
use_allow_list: false
|
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
|
||||||
@ -48,7 +48,7 @@ migrations_path: ./config/migrations
|
|||||||
auth:
|
auth:
|
||||||
# Can be 'rails' or 'jwt'
|
# Can be 'rails' or 'jwt'
|
||||||
type: rails
|
type: rails
|
||||||
cookie: _{{app_name_slug}}_session
|
cookie: _{% app_name_slug %}_session
|
||||||
|
|
||||||
# Comment this out if you want to disable setting
|
# Comment this out if you want to disable setting
|
||||||
# the user_id via a header for testing.
|
# the user_id via a header for testing.
|
||||||
@ -84,7 +84,7 @@ database:
|
|||||||
type: postgres
|
type: postgres
|
||||||
host: db
|
host: db
|
||||||
port: 5432
|
port: 5432
|
||||||
dbname: {{app_name_slug}}_development
|
dbname: {% app_name_slug %}_development
|
||||||
user: postgres
|
user: postgres
|
||||||
password: ''
|
password: ''
|
||||||
|
|
||||||
@ -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 } }"]
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
version: '3'
|
version: '3.4'
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres
|
image: postgres
|
||||||
|
@ -2,18 +2,17 @@
|
|||||||
# so I only need to overwrite some values
|
# so I only need to overwrite some values
|
||||||
inherits: dev
|
inherits: dev
|
||||||
|
|
||||||
app_name: "{{app_name}} Production"
|
app_name: "{% app_name %} Production"
|
||||||
host_port: 0.0.0.0:8080
|
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
|
||||||
# Disable this in development to get a list of
|
# from the allow list are permitted.
|
||||||
# queries used. When enabled super graph
|
# When it's 'false' all queries are saved to the
|
||||||
# will only allow queries from this list
|
# the allow list in ./config/allow.list
|
||||||
# List saved to ./config/allow.list
|
production: true
|
||||||
use_allow_list: true
|
|
||||||
|
|
||||||
# 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: true
|
auth_fail_block: true
|
||||||
@ -41,45 +40,11 @@ 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
|
||||||
port: 5432
|
port: 5432
|
||||||
dbname: {{app_name_slug}}_development
|
dbname: {% app_name_slug %}_development
|
||||||
user: postgres
|
user: postgres
|
||||||
password: ''
|
password: ''
|
||||||
#pool_size: 10
|
#pool_size: 10
|
||||||
|
Reference in New Issue
Block a user