Compare commits

...

17 Commits

Author SHA1 Message Date
aff2a13ba4 Added support for query names to the allow.list 2019-11-26 01:36:19 -05:00
f518d5fc69 Fix bug with compiling anon queries 2019-11-25 02:22:33 -05:00
7583326d21 Move sql query logging from info to debug 2019-11-22 01:32:09 -05:00
8024a8420b Use logger error instead of panic in goja handlers 2019-11-22 00:58:03 -05:00
6bd27d7c79 Add a db:reset command only for dev mode 2019-11-22 00:07:06 -05:00
a4c09dedd5 Optimize db queries limit use of transactions 2019-11-21 02:14:12 -05:00
176514b5f1 Added support for multi-root queries 2019-11-19 00:47:55 -05:00
396f4bcfd8 Fix issues with JWT auth 2019-11-15 01:35:19 -05:00
51c5f037f4 Fix bug with migration filename generation 2019-11-10 01:41:58 -05:00
c576f1ae16 Fix bug with migration file name 2019-11-10 01:39:24 -05:00
0a6995eaca Fix bug with migration template name 2019-11-10 01:35:06 -05:00
06ed823a51 Fix bug with creating new migrations 2019-11-10 01:33:06 -05:00
3438c3efb9 Fix macro syntax bug in app templates 2019-11-09 21:53:26 -05:00
605ec63466 Fix bugs and add new production mode 2019-11-07 02:37:24 -05:00
c7e63a6200 Update docs and website with new features 2019-11-05 00:43:32 -05:00
89bc93e159 Add nested where clause to filter based on related tables 2019-11-04 23:44:42 -05:00
77a51924a7 Block unauthorized requests when 'anon' role is not defined 2019-11-02 17:13:17 -04:00
60 changed files with 2568 additions and 2063 deletions

View File

@ -1,14 +1,14 @@
<a href="https://supergraph.dev"><img src="https://supergraph.dev/hologram.svg" width="100" height="100" align="right" /></a> <a href="https://supergraph.dev"><img src="https://supergraph.dev/hologram.svg" width="100" height="100" align="right" /></a>
# Super Graph - Build web products faster. Instant GraphQL APIs for your apps # Super Graph - Instant GraphQL APIs for your apps.
## Build web products faster. No code needed. GraphQL auto. transformed into efficient database queries.
![MIT license](https://img.shields.io/github/license/dosco/super-graph.svg) ![MIT license](https://img.shields.io/github/license/dosco/super-graph.svg)
![Docker build](https://img.shields.io/docker/cloud/build/dosco/super-graph.svg) ![Docker build](https://img.shields.io/docker/cloud/build/dosco/super-graph.svg)
![Cloud native](https://img.shields.io/badge/cloud--native-enabled-blue.svg) ![Cloud native](https://img.shields.io/badge/cloud--native-enabled-blue.svg)
[![Discord Chat](https://img.shields.io/discord/628796009539043348.svg)](https://discord.gg/6pSWCTZ) [![Discord Chat](https://img.shields.io/discord/628796009539043348.svg)](https://discord.gg/6pSWCTZ)
Get an instant high performance GraphQL API for Postgres. No code needed. GraphQL is automatically transformed into efficient database queries.
![GraphQL](docs/.vuepress/public/graphql.png?raw=true "") ![GraphQL](docs/.vuepress/public/graphql.png?raw=true "")
## The story of Super Graph? ## The story of Super Graph?
@ -25,20 +25,21 @@ This compiler is what sits at the heart of Super Graph with layers of useful fun
## Features ## Features
- Works with Rails database schemas - Role based access control
- Automatically learns schemas and relationships - Works with Ruby-On-Rails databases
- Belongs-To, One-To-Many and Many-To-Many table relationships - Automatically learns database schemas and relationships
- Full text search and Aggregations - Full text search and aggregations
- Rails Auth supported (Redis, Memcache, Cookie) - Rails authentication supported (Redis, Memcache, Cookie)
- JWT tokens supported (Auth0, etc) - JWT tokens supported (Auth0, etc)
- Join with remote REST APIs - Join database with remote REST APIs
- Highly optimized and fast Postgres SQL queries - Highly optimized and fast Postgres SQL queries
- Support GraphQL queries and mutations - GraphQL queries and mutations
- Configure with a simple config file - A simple config file
- High performance GO codebase - High performance GO codebase
- Tiny docker image and low memory requirements - Tiny docker image and low memory requirements
- Fuzz tested for security
- Database migrations tool - Database migrations tool
- Write database seeding scripts in Javascript - Database seeding tool
## Documentation ## Documentation

View File

@ -73,43 +73,6 @@ mutation {
} }
} }
variables {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query {
products {
id
name
}
}
variables {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query { query {
products { products {
id id
@ -133,21 +96,6 @@ mutation {
} }
} }
variables {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query { query {
products { products {
id id
@ -159,7 +107,6 @@ query {
} }
} }
variables { variables {
"data": { "data": {
"email": "gfk@myspace.com", "email": "gfk@myspace.com",
@ -175,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",
@ -225,50 +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 { query getProducts {
"data": [
{
"name": "Gumbo1",
"created_at": "now",
"updated_at": "now"
},
{
"name": "Gumbo2",
"created_at": "now",
"updated_at": "now"
}
]
}
query {
products { products {
id id
name name
description
users {
email
}
} }
} }

View File

@ -5,15 +5,14 @@ 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
# valid values: always, per_query, never auth_fail_block: false
auth_fail_block: never
# Latency tracing for database queries and remote joins # Latency tracing for database queries and remote joins
# the resulting latency information is returned with the # the resulting latency information is returned with the
@ -52,7 +51,8 @@ auth:
cookie: _app_session cookie: _app_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. Good for testing # the user_id via a header for testing.
# Disable in production
creds_in_header: true creds_in_header: true
rails: rails:
@ -93,14 +93,13 @@ database:
#max_retries: 0 #max_retries: 0
#log_level: "debug" #log_level: "debug"
# Define variables here that you want to use in filters # Set session variable "user.id" to the user id
# sub-queries must be wrapped in () # Enable this if you need the user id in triggers, etc
variables: set_user_id: false
account_id: "(select account_id from users where id = $user_id)"
# Define defaults to for the field key and values below # Define additional variables here to be used with filters
defaults: variables:
# filters: ["{ user_id: { eq: $user_id } }"] admin_account_id: "5"
# Field and table names that you wish to block # Field and table names that you wish to block
blocklist: blocklist:
@ -132,11 +131,12 @@ tables:
name: me name: me
table: users table: users
roles_query: "SELECT * FROM users as usr WHERE id = $user_id" 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
@ -170,7 +170,9 @@ 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 } }"]
@ -184,8 +186,7 @@ roles:
block: true block: true
- name: admin - name: admin
match: id = 1 match: id = 1000
tables: tables:
- name: users - name: users
# query: filters: []
# filters: ["{ account_id: { _eq: $account_id } }"]

View File

@ -9,15 +9,14 @@ 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
# valid values: always, per_query, never auth_fail_block: true
auth_fail_block: always
# Latency tracing for database queries and remote joins # Latency tracing for database queries and remote joins
# the resulting latency information is returned with the # the resulting latency information is returned with the
@ -42,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
@ -86,3 +51,7 @@ database:
#pool_size: 10 #pool_size: 10
#max_retries: 0 #max_retries: 0
#log_level: "debug" #log_level: "debug"
# Set session variable "user.id" to the user id
# Enable this if you need the user id in triggers, etc
set_user_id: false

View File

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

View File

@ -0,0 +1,19 @@
<template>
<div class="shadow bg-white p-4 flex items-start" :class="className">
<slot name="image"></slot>
<div class="pl-4">
<h2 class="p-0">
<slot name="title"></slot>
</h2>
<p>
<slot name="body"></slot>
</p>
</div>
</div>
</template>
<script>
export default {
props: ["className"]
}
</script>

View File

@ -23,6 +23,13 @@
class="inline-block px-4 py-3 my-8 bg-blue-600 text-blue-100 font-bold rounded" class="inline-block px-4 py-3 my-8 bg-blue-600 text-blue-100 font-bold rounded"
: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>
@ -51,17 +58,26 @@
</div> </div>
</div> </div>
<div class="bg-gray-100 mt-10"> <div class="bg-gray-100 mt-10">
<div class="container mx-auto px-10 md:px-0 py-32"> <div class="container mx-auto px-10 md:px-0 py-32">
<div class="pb-8 hidden md:block ">
<img src="arch-basic.svg">
</div>
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4"> <h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
What is {{ data.heroText }}? What is {{ data.heroText }}?
</h1> </h1>
<div class="text-2xl md:text-3xl"> <div class="text-2xl md:text-3xl">
Super Graph can automatically learn a Postgres database and instantly serve it as a fast and secured GraphQL API. It comes with tools to create a new app and manage it's database. You get it all, a very productive developer and a highly scalable app backend. It's designed to work well on serverless platforms by Google, AWS, Microsoft, etc. The goal is to save you a ton of time and money so you can focus on you're apps core value. Super Graph can automatically learn a Postgres database and instantly serve it as a fast and secured GraphQL API. It comes with tools to create a new app and manage it's database. You get it all, a very productive developer and a highly scalable app backend. It's designed to work well on serverless platforms by Google, AWS, Microsoft, etc. The goal is to save you a ton of time and money so you can focus on you're apps core value.
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="md:w-2/4"> <div class="md:w-2/4">
<img src="/graphql.png"> <img src="/graphql.png">
@ -112,7 +128,7 @@
</div> </div>
</div> </div>
<div class="overflow-hidden bg-indigo-900" style="height: 730px"> <div class="overflow-hidden bg-indigo-900">
<div class="container mx-auto py-20"> <div class="container mx-auto py-20">
<img src="/super-graph-web-ui.png"> <img src="/super-graph-web-ui.png">
</div> </div>
@ -143,10 +159,73 @@
</div> </div>
<div class="border-t py-10"> <div class="border-t py-10">
<div class="container mx-auto"> <div class="block md:hidden w-100">
<iframe src='https://www.youtube.com/embed/MfPL2A-DAJk' frameborder='0' allowfullscreen style="width: 100%; height: 250px;">
</iframe>
</div>
<div class="container mx-auto flex flex-col md:flex-row items-center">
<div class="w-100 md:w-1/2 p-8">
<h1 class="text-2xl font-bold">GraphQL the future of APIs</h1>
<p class="text-xl text-gray-600">Keeping a tight and fast development loop helps you iterate quickly. Leveraging technology like Super Graph focuses your team on building the core product and not reinventing wheels. GraphQL eliminate the dependency on the backend engineering and keeps the things moving fast</p>
</div>
<div class="hidden md:block md:w-1/2">
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style> <style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style>
<div class="embed-container shadow"> <div class="embed-container shadow">
<iframe src='https://www.youtube.com/embed/MfPL2A-DAJk' frameborder='0' allowfullscreen></iframe> <iframe src='https://www.youtube.com/embed/MfPL2A-DAJk' frameborder='0' allowfullscreen >
</iframe>
</div>
</div>
</div>
</div>
<div class="bg-gray-200 mt-10">
<div class="container mx-auto px-10 md:px-0 py-32">
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
Build Secure Apps
</h1>
<div class="flex flex-col text-2xl md:text-3xl">
<card className="mb-1 p-8">
<template #image><font-awesome-icon icon="portrait" class="text-red-500" /></template>
<template #title>Role Based Access Control</template>
<template #body>Dynamically assign roles like admin, manager or anon to specific users. Generate role specific queries at runtime. For example admins can get all users while others can only fetch their own user.</template>
</card>
<card className="mb-1 p-8">
<template #image><font-awesome-icon icon="shield-alt" class="text-blue-500" /></template>
<template #title>Prepared Statements</template>
<template #body>An additional layer of protection from a variety of security issues like SQL injection. In production mode all queries are precompiled into prepared statements so only those can be executed. This also significantly speeds up all queries.</template>
</card>
<card className="p-8">
<template #image><font-awesome-icon icon="lock" class="text-green-500"/></template>
<template #title>Fuzz Tested Code</template>
<template #body>Fuzzing is done by complex software that generates massives amounts of random input to detect if code is free of security bugs. Google uses fuzzing to protects everything from their cloud infrastructure to the Chrome browser.</template>
</card>
</div>
</div>
</div>
<div class="">
<div class="container mx-auto px-10 md:px-0 py-32">
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
More Features
</h1>
<div class="flex flex-col md:flex-row text-2xl md:text-3xl">
<card className="mr-0 md:mr-1 mb-1 flex-col w-100 md:w-1/3 items-center">
<template #image><img src="/arch-remote-join.svg" class="h-64"></template>
<template #title>Remote Joins</template>
<template #body>A powerful feature that allows you to query your database and remote REST APIs at the same time. For example fetch a user from the DB, his tweets from Twitter and his payments from Stripe with a single GraphQL query.</template>
</card>
<card className="mr-0 md:mr-1 mb-1 flex-col w-100 md:w-1/3">
<template #image><img src="/arch-search.svg" class="h-64"></template>
<template #title>Full Text Search</template>
<template #body>Postgres has excellent full-text search built-in. You don't need another expensive service. Super Graph makes it super easy to use with keyword ranking and highlighting also supported.</template>
</card>
<card className="mb-1 flex-col w-100 md:w-1/3">
<template #image><img src="/arch-bulk.svg" class="h-64"></template>
<template #title>Bulk Inserts</template>
<template #body>Efficiently insert, update and delete multiple items with a single query. Upserts are also supported</template>
</card>
</div> </div>
</div> </div>
</div> </div>
@ -164,9 +243,17 @@
<script> <script>
import NavLink from '@theme/components/NavLink.vue' import NavLink from '@theme/components/NavLink.vue'
import Navbar from '@theme/components/Navbar.vue' import Navbar from '@theme/components/Navbar.vue'
import Card from './Card.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faPortrait, faShieldAlt, faLock } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
library.add(faPortrait, faShieldAlt, faLock)
export default { export default {
components: { NavLink, Navbar }, components: { NavLink, Navbar, FontAwesomeIcon, Card },
computed: { computed: {
data () { data () {

View File

@ -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: {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="411px" height="240px" viewBox="-0.5 -0.5 411 240"><defs/><g><g transform="translate(1.5,6.5)"><switch><foreignObject style="overflow:visible;" pointer-events="none" width="379" height="232" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; white-space: nowrap; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;"><div><div style="font-size: 19px"><font color="#3b3b3b">[{ name: "beer1", description: "99 bottles of"},</font></div></div><div style="font-size: 19px"><div style="font-size: 12px"><div style="font-size: 19px"><font color="#3b3b3b"> { name: "beer2", description: "98 bottles of"},</font></div><div style="font-size: 19px"><div style="font-size: 12px"><div style="font-size: 19px"><font color="#3b3b3b"> { name: "beer3", description: "97 bottles of"},</font></div><div><div><div style="font-size: 19px"><font color="#3b3b3b"> { name: "beer4", description: "96 bottles of"},</font></div></div></div><div><div><div style="font-size: 19px"><font color="#3b3b3b"> { name: "beer5", description: "95 bottles of"},</font></div></div></div><div><div><div style="font-size: 19px"><font color="#3b3b3b"> { name: "beer6", description: "94 bottles of"},</font></div></div></div><div><div><div style="font-size: 19px"><font color="#3b3b3b"> { name: "beer7", description: "93 bottles of"},</font></div></div></div><div><div><div style="font-size: 19px"><font color="#3b3b3b"> { name: "beer8", description: "92 bottles of"}]</font></div></div></div><div><font color="#3b3b3b"><br /></font></div></div></div></div><div><font color="#3b3b3b"> </font></div></div><div style="font-size: 19px"><font color="#3b3b3b"> </font></div></div></div></foreignObject><text x="190" y="122" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">[Not supported by viewer]</text></switch></g></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="426px" height="204px" viewBox="-0.5 -0.5 426 204"><defs/><g><g transform="translate(1.5,6.5)"><switch><foreignObject style="overflow:visible;" pointer-events="none" width="411" height="196" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; white-space: nowrap; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;"><div><div style="font-size: 19px"><font color="#3b3b3b">query {</font></div><div><span style="font-size: 19px">  <font color="#cc0000">products</font><font color="#3b3b3b">(</font></span><span style="font-size: 19px"><font color="#3b3b3b">search: "</font><font color="#00994d">ale</font><font color="#3b3b3b">", where: { price: { gt: 3 }) {</font></span></div><div><span style="font-size: 19px">  <font color="#0066cc">  id</font></span></div><div><span style="font-size: 19px"><font color="#0066cc">    name</font></span></div><div><span style="font-size: 19px"><font color="#0066cc">    search_rank</font></span></div><div><span style="font-size: 19px"><font color="#0066cc">    search_headline_description</font><br /> <font color="#3b3b3b"> }<br />}</font></span></div></div><div style="font-size: 19px"><br /></div></div></div></foreignObject><text x="206" y="104" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">[Not supported by viewer]</text></switch></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -17,12 +17,12 @@ features:
details: Easy config file, quick to deploy, No code needed. It just works. details: Easy config file, quick to deploy, No code needed. It just works.
- title: High Performance - title: High Performance
details: Compiles your GraphQL into a fast SQL query in realtime. details: Compiles your GraphQL into a fast SQL query in realtime.
- title: Built in GO
details: Built in Go is a language created at Google to build fast and secure web services.
- title: Ruby-on-Rails - title: Ruby-on-Rails
details: Can read Rails cookies and supports rails database conventions. details: Can read Rails cookies and supports rails database conventions.
- title: Serverless - title: Serverless
details: Instant startup for scale to zero environments like Google Cloud Run, App Engine, AWS Lambda details: Instant startup for scale to zero environments like Google Cloud Run, App Engine, AWS Lambda
- title: Go Lang
details: Go is a language created at Google to build fast and secure web services.
- title: Free and Open Source - title: Free and Open Source
details: Not a VC funded startup. Not even a startup just good old open source code details: Not a VC funded startup. Not even a startup just good old open source code

View File

@ -9,20 +9,21 @@ Get an instant high performance GraphQL API for Postgres. No code needed. GraphQ
## Features ## Features
- Works with Rails database schemas - Role based access control
- Automatically learns schemas and relationships - Works with Ruby-On-Rails databases
- Belongs-To, One-To-Many and Many-To-Many table relationships - Automatically learns database schemas and relationships
- Full text search and Aggregations - Full text search and aggregations
- Rails Auth supported (Redis, Memcache, Cookie) - Rails authentication supported (Redis, Memcache, Cookie)
- JWT tokens supported (Auth0, etc) - JWT tokens supported (Auth0, etc)
- Join with remote REST APIs - Join database with remote REST APIs
- Highly optimized and fast Postgres SQL queries - Highly optimized and fast Postgres SQL queries
- Support GraphQL queries and mutations - GraphQL queries and mutations
- Configure with a simple config file - A simple config file
- High performance GO codebase - High performance GO codebase
- Tiny docker image and low memory requirements - Tiny docker image and low memory requirements
- Fuzz tested for security
- Database migrations tool - Database migrations tool
- Write database seeding scripts in Javascript - Database seeding tool
## Try the demo app ## Try the demo app
@ -275,7 +276,7 @@ transmission_gear_type
// Text // Text
word word
sentence sentence
paragrph paragraph
question question
quote quote
@ -475,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`.
@ -499,7 +515,36 @@ query {
### Advanced queries ### Advanced queries
Super Graph support complex queries where you can add filters, ordering,offsets and limits on the query. Super Graph support complex queries where you can add filters, ordering,offsets and limits on the query. For example the below query will list all products where the price is greater than 10 and the id is not 5.
```graphql
query {
products(where: {
and: {
price: { gt: 10 },
not: { id: { eq: 5 } }
}
}) {
name
price
}
}
```
#### Nested where clause targeting related tables
Sometimes you need to query a table based on a condition that applies to a related table. For example say you need to list all users who belong to an account. This query below will fetch the id and email or all users who belong to the account with id 3.
```graphql
query {
users(where: {
accounts: { id: { eq: 3 } }
}) {
id
email
}
}`
```
#### Logical Operators #### Logical Operators
@ -613,7 +658,7 @@ mutation {
} }
``` ```
### Bulk insert #### Bulk insert
```json ```json
{ {
@ -659,7 +704,7 @@ mutation {
} }
``` ```
### Bulk update #### Bulk update
```json ```json
{ {
@ -702,7 +747,7 @@ mutation {
} }
``` ```
### Bulk delete #### Bulk delete
```json ```json
{ {
@ -743,7 +788,7 @@ mutation {
} }
``` ```
### Bulk upsert #### Bulk upsert
```json ```json
{ {
@ -878,82 +923,39 @@ class AddSearchColumn < ActiveRecord::Migration[5.1]
end end
``` ```
## Remote Joins ## GraphQL with React
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. This is a quick simple example using `graphql.js` [https://github.com/f/graphql.js/](https://github.com/f/graphql.js/)
::: tip Is this fast? ```js
Super Graph is able fetch remote data and merge it with the DB response in an efficient manner. Several optimizations such as parallel HTTP requests and a zero-allocation JSON merge algorithm makes this very fast. All of this without you having to write a line of code. import React, { useState, useEffect } from 'react'
::: import graphql from 'graphql.js'
For example you need to list the last 3 payments made by a user. You will first need to look up the user in the database and then call the Stripe API to fetch his last 3 payments. For this to work your user table in the db has a `customer_id` column that contains his Stripe customer ID. // Create a GraphQL client pointing to Super Graph
var graph = graphql("http://localhost:3000/api/v1/graphql", { asJSON: true })
Similiarly you could also fetch the users last tweet, lead info from Salesforce or whatever else you need. It's fine to mix up several different `remote joins` into a single GraphQL query. const App = () => {
const [user, setUser] = useState(null)
### Stripe API example useEffect(() => {
async function action() {
The configuration is self explanatory. A `payments` field has been added under the `customers` table. This field is added to the `remotes` subsection that defines fields associated with `customers` that are remote and not real database columns. // Use the GraphQL client to execute a graphQL query
// The second argument to the client are the variables you need to pass
The `id` parameter maps a column from the `customers` table to the `$id` variable. In this case it maps `$id` to the `customer_id` column. const result = await graph(`{ user { id first_name last_name picture_url } }`)()
setUser(result)
```yaml
tables:
- name: customers
remotes:
- name: payments
id: stripe_id
url: http://rails_app:3000/stripe/$id
path: data
# debug: true
# pass_headers:
# - cookie
# - host
set_headers:
- name: Authorization
value: Bearer <stripe_api_key>
```
#### How do I make use of this?
Just include `payments` like you would any other GraphQL selector under the `customers` selector. Super Graph will call the configured API for you and stitch (merge) the JSON the API sends back with the JSON generated from the database query. GraphQL features like aliases and fields all work.
```graphql
query {
customers {
id
email
payments {
customer_id
amount
billing_details
}
} }
action()
}, []);
return (
<div className="App">
<h1>{ JSON.stringify(user) }</h1>
</div>
);
} }
``` ```
And voila here is the result. You get all of this advanced and honestly complex querying capability without writing a single line of code. export default App;
```json
"data": {
"customers": [
{
"id": 1,
"email": "linseymertz@reilly.co",
"payments": [
{
"customer_id": "cus_YCj3ndB5Mz",
"amount": 100,
"billing_details": {
"address": "1 Infinity Drive",
"zipcode": "94024"
}
},
...
```
Even tracing data is availble in the Super Graph web UI if tracing is enabled in the config. By default it is enabled in development. Additionally there you can set `debug: true` to enable http request / response dumping to help with debugging.
![Query Tracing](/tracing.png "Super Graph Web UI Query Tracing")
## Authentication ## Authentication
@ -1033,7 +1035,133 @@ We can get the JWT token either from the `authorization` header where we expect
For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file. For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
## Easy to setup ## Role based Access Control
It's a common usecase for APIs to control what information they return or insert based on the role of the user. For example when fetching a list of users, a normal user can only fetch his own entry while a manager can fetch all the users within a company and an admin user can fetch everyone. Or when creating a new user an an admin user can set a users role while the user himself cannot set or change it. This is called role based access control or RBAC.
Super Graph allows you to set access control rules based on dynamically defined roles. You can create as many roles as you wish. The only two default (built-in) roles are `user` for authenticated requests and `anon` for unauthenticated. An authenticated request is one where Super Graph can extract an `user_id` based on the configured authenication method (jwt, rails cookies, etc).
### Configure RBAC
```yaml
roles_query: "SELECT * FROM users WHERE users.id = $user_id"
roles:
- name: user
tables:
- name: users
query:
filters: ["{ id: { _eq: $user_id } }"]
insert:
filters: ["{ user_id: { eq: $user_id } }"]
columns: ["id", "name", "description" ]
presets:
- created_at: "now"
update:
filters: ["{ user_id: { eq: $user_id } }"]
columns:
- id
- name
presets:
- updated_at: "now"
delete:
block: true
- name: admin
match: users.id = 1
tables:
- name: users
query:
filters: []
```
This configuration is relatively simple to follow the `roles_query` parameter is the query that
must be run to help figure out a users role. This query can be as complex as you like and include joins with other tables.
The individual roles are defined under the `roles` parameter and this includes each table the role has a custom setting for. The role is dynamically matched using the `match` parameter for example in the above case `users.id = 1` means that when the `roles_query` is executed a user with the id `1` willbe assigned the admin role and those that don't match get the `user` role if authenticated successfully or the `anon` role.
## 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.
::: tip Is this fast?
Super Graph is able fetch remote data and merge it with the DB response in an efficient manner. Several optimizations such as parallel HTTP requests and a zero-allocation JSON merge algorithm makes this very fast. All of this without you having to write a line of code.
:::
For example you need to list the last 3 payments made by a user. You will first need to look up the user in the database and then call the Stripe API to fetch his last 3 payments. For this to work your user table in the db has a `customer_id` column that contains his Stripe customer ID.
Similiarly you could also fetch the users last tweet, lead info from Salesforce or whatever else you need. It's fine to mix up several different `remote joins` into a single GraphQL query.
### Stripe API example
The configuration is self explanatory. A `payments` field has been added under the `customers` table. This field is added to the `remotes` subsection that defines fields associated with `customers` that are remote and not real database columns.
The `id` parameter maps a column from the `customers` table to the `$id` variable. In this case it maps `$id` to the `customer_id` column.
```yaml
tables:
- name: customers
remotes:
- name: payments
id: stripe_id
url: http://rails_app:3000/stripe/$id
path: data
# debug: true
# pass_headers:
# - cookie
# - host
set_headers:
- name: Authorization
value: Bearer <stripe_api_key>
```
#### How do I make use of this?
Just include `payments` like you would any other GraphQL selector under the `customers` selector. Super Graph will call the configured API for you and stitch (merge) the JSON the API sends back with the JSON generated from the database query. GraphQL features like aliases and fields all work.
```graphql
query {
customers {
id
email
payments {
customer_id
amount
billing_details
}
}
}
```
And voila here is the result. You get all of this advanced and honestly complex querying capability without writing a single line of code.
```json
"data": {
"customers": [
{
"id": 1,
"email": "linseymertz@reilly.co",
"payments": [
{
"customer_id": "cus_YCj3ndB5Mz",
"amount": 100,
"billing_details": {
"address": "1 Infinity Drive",
"zipcode": "94024"
}
},
...
```
Even tracing data is availble in the Super Graph web UI if tracing is enabled in the config. By default it is enabled in development. Additionally there you can set `debug: true` to enable http request / response dumping to help with debugging.
![Query Tracing](/tracing.png "Super Graph Web UI Query Tracing")
## Configuration files
Configuration files can either be in YAML or JSON their names are derived from the `GO_ENV` variable, for example `GO_ENV=prod` will cause the `prod.yaml` config file to be used. or `GO_ENV=dev` will use the `dev.yaml`. A path to look for the config files in can be specified using the `-path <folder>` command line argument. Configuration files can either be in YAML or JSON their names are derived from the `GO_ENV` variable, for example `GO_ENV=prod` will cause the `prod.yaml` config file to be used. or `GO_ENV=dev` will use the `dev.yaml`. A path to look for the config files in can be specified using the `-path <folder>` command line argument.
@ -1051,15 +1179,14 @@ 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
# valid values: always, per_query, never auth_fail_block: false
auth_fail_block: never
# Latency tracing for database queries and remote joins # Latency tracing for database queries and remote joins
# the resulting latency information is returned with the # the resulting latency information is returned with the
@ -1098,7 +1225,8 @@ auth:
cookie: _app_session cookie: _app_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. Good for testing # the user_id via a header for testing.
# Disable in production
creds_in_header: true creds_in_header: true
rails: rails:
@ -1139,14 +1267,13 @@ database:
#max_retries: 0 #max_retries: 0
#log_level: "debug" #log_level: "debug"
# Define variables here that you want to use in filters # Set session variable "user.id" to the user id
# sub-queries must be wrapped in () # Enable this if you need the user id in triggers, etc
variables: set_user_id: false
account_id: "(select account_id from users where id = $user_id)"
# Define defaults to for the field key and values below # Define additional variables here to be used with filters
defaults: variables:
# filters: ["{ user_id: { eq: $user_id } }"] admin_account_id: "5"
# Field and table names that you wish to block # Field and table names that you wish to block
blocklist: blocklist:
@ -1178,7 +1305,7 @@ tables:
name: me name: me
table: users table: users
roles_query: "SELECT * FROM users as usr WHERE id = $user_id" roles_query: "SELECT * FROM users WHERE id = $user_id"
roles: roles:
- name: anon - name: anon
@ -1227,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 } }"]
``` ```

View File

@ -10,5 +10,10 @@
"tailwindcss": "^1.0.6", "tailwindcss": "^1.0.6",
"vuepress": "^1.0.0", "vuepress": "^1.0.0",
"webpack-dev-middleware": "3.6.0" "webpack-dev-middleware": "3.6.0"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/vue-fontawesome": "^0.1.7"
} }
} }

View File

@ -10,17 +10,17 @@
"@babel/highlight" "^7.0.0" "@babel/highlight" "^7.0.0"
"@babel/core@^7.0.0": "@babel/core@^7.0.0":
version "7.6.2" version "7.6.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.2.tgz#069a776e8d5e9eefff76236bc8845566bd31dd91" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.4.tgz#6ebd9fe00925f6c3e177bb726a188b5f578088ff"
integrity sha512-l8zto/fuoZIbncm+01p8zPSDZu/VuuJhAfA7d/AbzM09WR7iVhavvfNDYCNpo1VvLk6E6xgAoP9P+/EMJHuRkQ== integrity sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ==
dependencies: dependencies:
"@babel/code-frame" "^7.5.5" "@babel/code-frame" "^7.5.5"
"@babel/generator" "^7.6.2" "@babel/generator" "^7.6.4"
"@babel/helpers" "^7.6.2" "@babel/helpers" "^7.6.2"
"@babel/parser" "^7.6.2" "@babel/parser" "^7.6.4"
"@babel/template" "^7.6.0" "@babel/template" "^7.6.0"
"@babel/traverse" "^7.6.2" "@babel/traverse" "^7.6.3"
"@babel/types" "^7.6.0" "@babel/types" "^7.6.3"
convert-source-map "^1.1.0" convert-source-map "^1.1.0"
debug "^4.1.0" debug "^4.1.0"
json5 "^2.1.0" json5 "^2.1.0"
@ -29,12 +29,12 @@
semver "^5.4.1" semver "^5.4.1"
source-map "^0.5.0" source-map "^0.5.0"
"@babel/generator@^7.6.2": "@babel/generator@^7.6.3", "@babel/generator@^7.6.4":
version "7.6.2" version "7.6.4"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.2.tgz#dac8a3c2df118334c2a29ff3446da1636a8f8c03" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671"
integrity sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ== integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w==
dependencies: dependencies:
"@babel/types" "^7.6.0" "@babel/types" "^7.6.3"
jsesc "^2.5.1" jsesc "^2.5.1"
lodash "^4.17.13" lodash "^4.17.13"
source-map "^0.5.0" source-map "^0.5.0"
@ -224,10 +224,10 @@
esutils "^2.0.2" esutils "^2.0.2"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/parser@^7.6.0", "@babel/parser@^7.6.2": "@babel/parser@^7.6.0", "@babel/parser@^7.6.3", "@babel/parser@^7.6.4":
version "7.6.2" version "7.6.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81"
integrity sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg== integrity sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A==
"@babel/plugin-proposal-async-generator-functions@^7.2.0": "@babel/plugin-proposal-async-generator-functions@^7.2.0":
version "7.2.0" version "7.2.0"
@ -361,9 +361,9 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-block-scoping@^7.3.4": "@babel/plugin-transform-block-scoping@^7.3.4":
version "7.6.2" version "7.6.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.2.tgz#96c33ab97a9ae500cc6f5b19e04a7e6553360a79" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.3.tgz#6e854e51fbbaa84351b15d4ddafe342f3a5d542a"
integrity sha512-zZT8ivau9LOQQaOGC7bQLQOT4XPkPXgN2ERfUgk1X8ql+mVkLc4E8eKk+FO3o0154kxzqenWCorfmEXpEZcrSQ== integrity sha512-7hvrg75dubcO3ZI2rjYTzUrEuh1E9IyDEhhB6qfcooxhDA33xx2MasuLVgdxzcP6R/lipAC6n9ub9maNW6RKdw==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
lodash "^4.17.13" lodash "^4.17.13"
@ -479,9 +479,9 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": "@babel/plugin-transform-named-capturing-groups-regex@^7.3.0":
version "7.6.2" version "7.6.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.6.2.tgz#c1ca0bb84b94f385ca302c3932e870b0fb0e522b" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.6.3.tgz#aaa6e409dd4fb2e50b6e2a91f7e3a3149dbce0cf"
integrity sha512-xBdB+XOs+lgbZc2/4F5BVDVcDNS4tcSKQc96KmlqLEAwz6tpYPEvPdmDfvVG0Ssn8lAhronaRs6Z6KSexIpK5g== integrity sha512-jTkk7/uE6H2s5w6VlMHeWuH+Pcy2lmdwFoeWCVnvIrDUnB5gQqTVI8WfmEAhF2CDEarGrknZcmSFg1+bkfCoSw==
dependencies: dependencies:
regexpu-core "^4.6.0" regexpu-core "^4.6.0"
@ -622,17 +622,17 @@
semver "^5.3.0" semver "^5.3.0"
"@babel/runtime-corejs2@^7.2.0": "@babel/runtime-corejs2@^7.2.0":
version "7.6.2" version "7.6.3"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.6.2.tgz#062f8e31f3df30fc1a3dea68aa1bd854e06e9ba6" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.6.3.tgz#de3f446b3fb688b98cbd220474d1a7cad909bcb8"
integrity sha512-wdyVKnTv9Be4YlwF/7pByYNfcl23qC21aAQ0aIaZOo2ZOvhFEyJdBLJClYZ9i+Pmrz7sUQgg/MwbJa2RZTkygg== integrity sha512-nuA2o+rgX2+PrNTZ063ehncVcg7sn+tU71BB81SaWRVUbGwCOlb0+yQA1e0QqmzOfRSYOxfvf8cosYqFbJEiwQ==
dependencies: dependencies:
core-js "^2.6.5" core-js "^2.6.5"
regenerator-runtime "^0.13.2" regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0": "@babel/runtime@^7.0.0":
version "7.6.2" version "7.6.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.2.tgz#c3d6e41b304ef10dcf13777a33e7694ec4a9a6dd" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.3.tgz#935122c74c73d2240cafd32ddb5fc2a6cd35cf1f"
integrity sha512-EXxN64agfUqqIGeEjI5dL5z0Sw0ZwWo1mLTi4mQowCZ42O59b7DRpZAnTC6OqdF28wMBMFKNb/4uFGrVaigSpg== integrity sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA==
dependencies: dependencies:
regenerator-runtime "^0.13.2" regenerator-runtime "^0.13.2"
@ -645,31 +645,55 @@
"@babel/parser" "^7.6.0" "@babel/parser" "^7.6.0"
"@babel/types" "^7.6.0" "@babel/types" "^7.6.0"
"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5", "@babel/traverse@^7.6.2": "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5", "@babel/traverse@^7.6.2", "@babel/traverse@^7.6.3":
version "7.6.2" version "7.6.3"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.2.tgz#b0e2bfd401d339ce0e6c05690206d1e11502ce2c" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.3.tgz#66d7dba146b086703c0fb10dd588b7364cec47f9"
integrity sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ== integrity sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw==
dependencies: dependencies:
"@babel/code-frame" "^7.5.5" "@babel/code-frame" "^7.5.5"
"@babel/generator" "^7.6.2" "@babel/generator" "^7.6.3"
"@babel/helper-function-name" "^7.1.0" "@babel/helper-function-name" "^7.1.0"
"@babel/helper-split-export-declaration" "^7.4.4" "@babel/helper-split-export-declaration" "^7.4.4"
"@babel/parser" "^7.6.2" "@babel/parser" "^7.6.3"
"@babel/types" "^7.6.0" "@babel/types" "^7.6.3"
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
lodash "^4.17.13" lodash "^4.17.13"
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.6.0": "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.6.0", "@babel/types@^7.6.3":
version "7.6.1" version "7.6.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09"
integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g== integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA==
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@fullhuman/postcss-purgecss@^1.1.0": "@fortawesome/fontawesome-common-types@^0.2.25":
version "0.2.25"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.25.tgz#6df015905081f2762e5cfddeb7a20d2e9b16c786"
integrity sha512-3RuZPDuuPELd7RXtUqTCfed14fcny9UiPOkdr2i+cYxBoTOfQgxcDoq77fHiiHcgWuo1LoBUpvGxFF1H/y7s3Q==
"@fortawesome/fontawesome-svg-core@^1.2.25":
version "1.2.25"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.25.tgz#24b03391d14f0c6171e8cad7057c687b74049790"
integrity sha512-MotKnn53JKqbkLQiwcZSBJVYtTgIKFbh7B8+kd05TSnfKYPFmjKKI59o2fpz5t0Hzl35vVGU6+N4twoOpZUrqA==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.25"
"@fortawesome/free-solid-svg-icons@^5.11.2":
version "5.11.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.11.2.tgz#2f2f1459743a27902b76655a0d0bc5ec4d945631"
integrity sha512-zBue4i0PAZJUXOmLBBvM7L0O7wmsDC8dFv9IhpW5QL4kT9xhhVUsYg/LX1+5KaukWq4/cbDcKT+RT1aRe543sg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.25"
"@fortawesome/vue-fontawesome@^0.1.7":
version "0.1.7"
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.1.7.tgz#121867297cafd141af78c67d92ab9f1ad4b7328b"
integrity sha512-YCw2Q2m4fxzyFsPOH3uDYMoJztTD+pT+AAyse4LFpbdrBg+r8ueaVT8BFnXEjrGwMDJJeXrwJ5AOC6q/JWBI4w==
"@fullhuman/postcss-purgecss@^1.3.0":
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-1.3.0.tgz#d632900d818f4fcf4678e7326923fb838c3e03a7" resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-1.3.0.tgz#d632900d818f4fcf4678e7326923fb838c3e03a7"
integrity sha512-zvfS3dPKD2FAtMcXapMJXGbDgEp9E++mLR6lTgSruv6y37uvV5xJ1crVktuC1gvnmMwsa7Zh1m05FeEiz4VnIQ== integrity sha512-zvfS3dPKD2FAtMcXapMJXGbDgEp9E++mLR6lTgSruv6y37uvV5xJ1crVktuC1gvnmMwsa7Zh1m05FeEiz4VnIQ==
@ -691,13 +715,13 @@
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@silvanite/vuepress-plugin-tailwind@^1.1.0": "@silvanite/vuepress-plugin-tailwind@^1.1.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@silvanite/vuepress-plugin-tailwind/-/vuepress-plugin-tailwind-1.1.0.tgz#e5d0c8b0b1127201509196eca359ac9070517202" resolved "https://registry.yarnpkg.com/@silvanite/vuepress-plugin-tailwind/-/vuepress-plugin-tailwind-1.2.0.tgz#2e1e6d10e441b49c7446bbb99e7fe8ec80aee273"
integrity sha512-pQVMz0knDMMfIuXhEwYSRK2gPW9ds+C9YEX8IF0sdCqiPRlJSNG5oUTIAoKoe2JYHlr7zdQZX8wBQN7FQEVO4Q== integrity sha512-1LWlIa+g1vV6HkwwEmADAhnuZ33oAGidnbrCs7qnK53ApbtWa9K/+0cHCLhDeQ3zqoyotqK7YQR2sm5nCOTYqA==
dependencies: dependencies:
"@fullhuman/postcss-purgecss" "^1.1.0" "@fullhuman/postcss-purgecss" "^1.3.0"
lodash "^4.17.11" lodash "^4.17.15"
tailwindcss "^0.7.4" tailwindcss "^1.1.2"
"@types/events@*": "@types/events@*":
version "3.0.0" version "3.0.0"
@ -719,9 +743,9 @@
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*": "@types/node@*":
version "12.7.8" version "12.12.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.8.tgz#cb1bf6800238898bc2ff6ffa5702c3cadd350708" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.3.tgz#ebfe83507ac506bc3486314a8aa395be66af8d23"
integrity sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A== integrity sha512-opgSsy+cEF9N8MgaVPnWVtdJ3o4mV2aMHvDq7thkQUFt0EuOHJon4rQpJfhjmNHB+ikl0Cd6WhWIErOyQ+f7tw==
"@types/q@^1.5.1": "@types/q@^1.5.1":
version "1.5.2" version "1.5.2"
@ -746,9 +770,9 @@
svg-tags "^1.0.0" svg-tags "^1.0.0"
"@vue/babel-preset-app@^3.1.1": "@vue/babel-preset-app@^3.1.1":
version "3.11.0" version "3.12.1"
resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-3.11.0.tgz#52bf79c15560a304a13f4770e3e5530e01dd6173" resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-3.12.1.tgz#24c477052f078f30fdb7735103b14dd1fa2cbfe1"
integrity sha512-fcCq9nuGGx1WGnyaKHvIC8RnWjISXGf1rJH4mN9+bymDfosgDbwnfV4TYvTZlyK1/aTHEEpIoO3XimTXBo7QBw== integrity sha512-Zjy5jQaikV1Pz+ri0YgXFS7q4/5wCxB5tRkDOEIt5+4105u0Feb/pvH20nVL6nx9GyXrECFfcm7Yxr/z++OaPQ==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.0.0" "@babel/helper-module-imports" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"
@ -765,15 +789,15 @@
core-js "^2.6.5" core-js "^2.6.5"
"@vue/babel-preset-jsx@^1.0.0": "@vue/babel-preset-jsx@^1.0.0":
version "1.1.0" version "1.1.1"
resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.1.0.tgz#c8001329f5b372297a3111a251eb4f9e956c1266" resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.1.1.tgz#3a74642ca0ecea10aae13649df5ff70f9d24a6f5"
integrity sha512-EeZ9gwEmu79B4A6LMLAw5cPCVYIcbKWgJgJafWtLzh1S+SgERUmTkVQ9Vx4k8zYBiCuxHK3XziZ3VJIMau7THA== integrity sha512-SeyndwQZc8MAOkhbJaC34ocTwcKekKkwrwnTMC3YF8VmGp5IQWW5gPIU66bqO9WFBXFA3J3ANsUbP2pj8q8KdQ==
dependencies: dependencies:
"@vue/babel-helper-vue-jsx-merge-props" "^1.0.0" "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0"
"@vue/babel-plugin-transform-vue-jsx" "^1.0.0" "@vue/babel-plugin-transform-vue-jsx" "^1.0.0"
"@vue/babel-sugar-functional-vue" "^1.0.0" "@vue/babel-sugar-functional-vue" "^1.0.0"
"@vue/babel-sugar-inject-h" "^1.0.0" "@vue/babel-sugar-inject-h" "^1.0.0"
"@vue/babel-sugar-v-model" "^1.0.0" "@vue/babel-sugar-v-model" "^1.1.1"
"@vue/babel-sugar-v-on" "^1.1.0" "@vue/babel-sugar-v-on" "^1.1.0"
"@vue/babel-sugar-functional-vue@^1.0.0": "@vue/babel-sugar-functional-vue@^1.0.0":
@ -790,10 +814,10 @@
dependencies: dependencies:
"@babel/plugin-syntax-jsx" "^7.2.0" "@babel/plugin-syntax-jsx" "^7.2.0"
"@vue/babel-sugar-v-model@^1.0.0": "@vue/babel-sugar-v-model@^1.1.1":
version "1.0.0" version "1.1.1"
resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.0.0.tgz#f4da56aa67f65a349bd2c269a95e72e601af4613" resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.1.1.tgz#a0f0750fcee20769805a20178299eebd4babf25a"
integrity sha512-Pfg2Al0io66P1eO6zUbRIgpyKCU2qTnumiE0lao/wA/uNdb7Dx5Tfd1W6tO5SsByETPnEs8i8+gawRIXX40rFw== integrity sha512-qiPbdUTiqNQdhXzvWQMVfrYGHCiMmscY7j/cudLxdxWZ8AFhgPRVlniVgaWIT7A1iOjs92e8U6qVyqkf0d4ZrA==
dependencies: dependencies:
"@babel/plugin-syntax-jsx" "^7.2.0" "@babel/plugin-syntax-jsx" "^7.2.0"
"@vue/babel-helper-vue-jsx-merge-props" "^1.0.0" "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0"
@ -826,18 +850,18 @@
source-map "~0.6.1" source-map "~0.6.1"
vue-template-es2015-compiler "^1.9.0" vue-template-es2015-compiler "^1.9.0"
"@vuepress/core@^1.1.0": "@vuepress/core@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/core/-/core-1.1.0.tgz#32fd2b65a4613085cbd2b812bf67afe3a037dc65" resolved "https://registry.yarnpkg.com/@vuepress/core/-/core-1.2.0.tgz#8e0c636b7f8676202fdd1ecfbe31bfe245dab2a8"
integrity sha512-qC+R9kdTpui9QjQGUXUsmfAbToWOnoYjP2AJqMT/RsKUhQsXAIMe2Z0L/Vw2Z3bmlTUq26v+B1zlFgYzGuyIEQ== integrity sha512-ZIsUkQIF+h4Yk6q4okoRnRwRhcYePu/kNiL0WWPDGycjai8cFqFjLDP/tJjfTKXmn9A62j2ETjSwaiMxCtDkyw==
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@vue/babel-preset-app" "^3.1.1" "@vue/babel-preset-app" "^3.1.1"
"@vuepress/markdown" "^1.1.0" "@vuepress/markdown" "^1.2.0"
"@vuepress/markdown-loader" "^1.1.0" "@vuepress/markdown-loader" "^1.2.0"
"@vuepress/plugin-last-updated" "^1.1.0" "@vuepress/plugin-last-updated" "^1.2.0"
"@vuepress/plugin-register-components" "^1.1.0" "@vuepress/plugin-register-components" "^1.2.0"
"@vuepress/shared-utils" "^1.1.0" "@vuepress/shared-utils" "^1.2.0"
autoprefixer "^9.5.1" autoprefixer "^9.5.1"
babel-loader "^8.0.4" babel-loader "^8.0.4"
cache-loader "^3.0.0" cache-loader "^3.0.0"
@ -856,34 +880,34 @@
postcss-safe-parser "^4.0.1" postcss-safe-parser "^4.0.1"
toml "^3.0.0" toml "^3.0.0"
url-loader "^1.0.1" url-loader "^1.0.1"
vue "^2.5.16" vue "^2.6.10"
vue-loader "^15.2.4" vue-loader "^15.7.1"
vue-router "^3.0.2" vue-router "^3.1.3"
vue-server-renderer "^2.5.16" vue-server-renderer "^2.6.10"
vue-template-compiler "^2.5.16" vue-template-compiler "^2.6.10"
vuepress-html-webpack-plugin "^3.2.0" vuepress-html-webpack-plugin "^3.2.0"
vuepress-plugin-container "^2.0.0" vuepress-plugin-container "^2.0.2"
webpack "^4.8.1" webpack "^4.8.1"
webpack-chain "^4.6.0" webpack-chain "^4.6.0"
webpack-dev-server "^3.5.1" webpack-dev-server "^3.5.1"
webpack-merge "^4.1.2" webpack-merge "^4.1.2"
webpackbar "3.2.0" webpackbar "3.2.0"
"@vuepress/markdown-loader@^1.1.0": "@vuepress/markdown-loader@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/markdown-loader/-/markdown-loader-1.1.0.tgz#ab8ac2d286c255f9fa39ecb2f4542053314825ac" resolved "https://registry.yarnpkg.com/@vuepress/markdown-loader/-/markdown-loader-1.2.0.tgz#f8972014616b4ab46a99c9aaac2dd414d437411c"
integrity sha512-X4+E9kbFt3OSXKxtQbNxeuzxbXdSMhXz8tliUW+/+1zx7RGn1ApcR0x7Y6/irESUgZ+GxOT3jyiCDZA4usHhLA== integrity sha512-gOZzoHjfp/W6t+qKBRdbHS/9TwRnNuhY7V+yFzxofNONFHQULofIN/arG+ptYc2SuqJ541jqudNQW+ldHNMC2w==
dependencies: dependencies:
"@vuepress/markdown" "^1.1.0" "@vuepress/markdown" "^1.2.0"
loader-utils "^1.1.0" loader-utils "^1.1.0"
lru-cache "^5.1.1" lru-cache "^5.1.1"
"@vuepress/markdown@^1.1.0": "@vuepress/markdown@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/markdown/-/markdown-1.1.0.tgz#f9095c91019d21dbc3daedfd3773c6d5c29117ec" resolved "https://registry.yarnpkg.com/@vuepress/markdown/-/markdown-1.2.0.tgz#7c457e0fab52ef8ac4dd1898ae450bc3aec30746"
integrity sha512-O2ivsIkUrSUPDx+9N43XKSOGtprV4G1k6/4o3wZjjCn6GXYRsRE906cFDlbryHxQ49Z7Yfz3gyZIGMnThxLo/w== integrity sha512-RLRQmTu5wJbCO4Qv+J0K53o5Ew7nAGItLwWyzCbIUB6pRsya3kqSCViWQVlKlS53zFTmRHuAC9tJMRdzly3mCA==
dependencies: dependencies:
"@vuepress/shared-utils" "^1.1.0" "@vuepress/shared-utils" "^1.2.0"
markdown-it "^8.4.1" markdown-it "^8.4.1"
markdown-it-anchor "^5.0.2" markdown-it-anchor "^5.0.2"
markdown-it-chain "^1.3.0" markdown-it-chain "^1.3.0"
@ -891,43 +915,43 @@
markdown-it-table-of-contents "^0.4.0" markdown-it-table-of-contents "^0.4.0"
prismjs "^1.13.0" prismjs "^1.13.0"
"@vuepress/plugin-active-header-links@^1.1.0": "@vuepress/plugin-active-header-links@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/plugin-active-header-links/-/plugin-active-header-links-1.1.0.tgz#cd62c1712040676035f34fed16a088e1c08811d8" resolved "https://registry.yarnpkg.com/@vuepress/plugin-active-header-links/-/plugin-active-header-links-1.2.0.tgz#46495c89e51a95e57139be007dffbcae4b229260"
integrity sha512-sa5ySYl/kTyr1AMakeW375wWs1aQ6psiJiSFclxkGvxcuGZ89F27ELvd43DKaETAlH90LcoE/j7TXMA895qXmw== integrity sha512-vdi7l96pElJvEmcx6t9DWJNH25TIurS8acjN3+b7o4NzdaszFn5j6klN6WtI4Z+5BVDrxHP5W1F3Ebw8SZyupA==
dependencies: dependencies:
lodash.throttle "^4.1.1" lodash.debounce "^4.0.8"
"@vuepress/plugin-last-updated@^1.1.0": "@vuepress/plugin-last-updated@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/plugin-last-updated/-/plugin-last-updated-1.1.0.tgz#65f2de734f3744026297b4667f3b5276ef99fd06" resolved "https://registry.yarnpkg.com/@vuepress/plugin-last-updated/-/plugin-last-updated-1.2.0.tgz#7b34065b793848b0482a222b7a6f1b7df3668cdc"
integrity sha512-x2SaAKWk26RK9O0slnZ55eSlBFYdYjFgqkRIfaOf4f2biWqTa9nzaIbvjzvcx3AZKlOWMl81KRwybhDL8E9OsA== integrity sha512-j4uZb/MXDyG+v9QCG3T/rkiaOhC/ib7NKCt1cjn3GOwvWTDmB5UZm9EBhUpbDNrBgxW+SaHOe3kMVNO8bGOTGw==
dependencies: dependencies:
cross-spawn "^6.0.5" cross-spawn "^6.0.5"
"@vuepress/plugin-nprogress@^1.1.0": "@vuepress/plugin-nprogress@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/plugin-nprogress/-/plugin-nprogress-1.1.0.tgz#ca7106adc7016ed0d90a22555066c11da597ef59" resolved "https://registry.yarnpkg.com/@vuepress/plugin-nprogress/-/plugin-nprogress-1.2.0.tgz#ff6166946a0b118a39a562acb57983529afce4d2"
integrity sha512-XhUyAO+mzYFOFupX/pNlPbv0bT596Lk000Q2PhWfRliwUzpUd0/u5Z6B6fasIVj01Yqih/gAGOZpr2ZwSCNJYw== integrity sha512-0apt3Dp6XVCOkLViX6seKSEJgARihi+pX3/r8j8ndFp9Y+vmgLFZnQnGE5iKNi1ty+A6PZOK0RQcBjwTAU4pAw==
dependencies: dependencies:
nprogress "^0.2.0" nprogress "^0.2.0"
"@vuepress/plugin-register-components@^1.1.0": "@vuepress/plugin-register-components@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/plugin-register-components/-/plugin-register-components-1.1.0.tgz#42ea75bcad3fb562fbb86c424136f86e13641162" resolved "https://registry.yarnpkg.com/@vuepress/plugin-register-components/-/plugin-register-components-1.2.0.tgz#95aa0e0af94b2758b26ab98814c43b0f7bcd502b"
integrity sha512-HXGdcmBdGHLhI8KHr09GnnZEzgCuaIQx1WBqDNfbigSVKEx910L56ej+Whl6VFd7D0uOLUlW4kb9ELM0sjJpKg== integrity sha512-C32b8sbGtDEX8I3SUUKS/w2rThiRFiKxmzNcJD996me7VY/4rgmZ8CxGtb6G9wByVoK0UdG1SOkrgOPdSCm80A==
dependencies: dependencies:
"@vuepress/shared-utils" "^1.1.0" "@vuepress/shared-utils" "^1.2.0"
"@vuepress/plugin-search@^1.1.0": "@vuepress/plugin-search@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/plugin-search/-/plugin-search-1.1.0.tgz#3b7a344a7df1bab27f10a46e6b57680c8f5d4c7e" resolved "https://registry.yarnpkg.com/@vuepress/plugin-search/-/plugin-search-1.2.0.tgz#0b27c467b7fd42bd4d9e32de0fe2fb81a24bd311"
integrity sha512-GoxvcM65ZAZycnsoZJ/wx9F3hXKzzJQdS7lNnAuHrvCheT5tVO1wwMumVP/unZU/59zCQ1PiyReYntLSp5bXVg== integrity sha512-QU3JfnMfImDAArbJOVH1q1iCDE5QrT99GLpNGo6KQYZWqY1TWAbgyf8C2hQdaI03co1lkU2Wn/iqzHJ5WHlueg==
"@vuepress/shared-utils@^1.1.0": "@vuepress/shared-utils@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/shared-utils/-/shared-utils-1.1.0.tgz#9d220ffe54f2d698c56ca5348ba2cb9dd72800da" resolved "https://registry.yarnpkg.com/@vuepress/shared-utils/-/shared-utils-1.2.0.tgz#8d9ab40c24f75f027ef32c2ad0169f0f08e949fa"
integrity sha512-zvYfejRRl7y3oavLvAe7dHfCu4XewKnhsyUQ7to6tfxVNoEqzhrl5HcCBwcLlphj792tvTAth5QkVegTgGfsaw== integrity sha512-wo5Ng2/xzsmIYCzvWxgLFlDBp7FkmJp2shAkbSurLNAh1vixhs0+LyDxsk01+m34ktJSp9rTUUsm6khw/Fvo0w==
dependencies: dependencies:
chalk "^2.3.2" chalk "^2.3.2"
diacritics "^1.3.0" diacritics "^1.3.0"
@ -939,19 +963,20 @@
semver "^6.0.0" semver "^6.0.0"
upath "^1.1.0" upath "^1.1.0"
"@vuepress/theme-default@^1.1.0": "@vuepress/theme-default@^1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@vuepress/theme-default/-/theme-default-1.1.0.tgz#915c97bb69985d6fccd815f829532d67d828e10a" resolved "https://registry.yarnpkg.com/@vuepress/theme-default/-/theme-default-1.2.0.tgz#3303af21a00031a3482ed1c494508234f545cbf1"
integrity sha512-U+kFHakSBEXFAdfItyeCbP//q2hm9R8+vnTFjbMMVgRZ2SHPnDUC/7WWGoEUzfEpFHHPrG1OzC9iI/o5v8p5AQ== integrity sha512-mJxAMYQQv4OrGFsArMlONu8RpCzPUVx81dumkyTT4ay5PXAWTj+WDeFQLOT3j0g9QrDJGnHhbiw2aS+R/0WUyQ==
dependencies: dependencies:
"@vuepress/plugin-active-header-links" "^1.1.0" "@vuepress/plugin-active-header-links" "^1.2.0"
"@vuepress/plugin-nprogress" "^1.1.0" "@vuepress/plugin-nprogress" "^1.2.0"
"@vuepress/plugin-search" "^1.1.0" "@vuepress/plugin-search" "^1.2.0"
docsearch.js "^2.5.2" docsearch.js "^2.5.2"
lodash "^4.17.15" lodash "^4.17.15"
stylus "^0.54.5" stylus "^0.54.5"
stylus-loader "^3.0.2" stylus-loader "^3.0.2"
vuepress-plugin-container "^2.0.0" vuepress-plugin-container "^2.0.2"
vuepress-plugin-smooth-scroll "^0.0.3"
"@webassemblyjs/ast@1.8.5": "@webassemblyjs/ast@1.8.5":
version "1.8.5" version "1.8.5"
@ -1153,9 +1178,9 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.5.5:
uri-js "^4.2.2" uri-js "^4.2.2"
algoliasearch@^3.24.5: algoliasearch@^3.24.5:
version "3.35.0" version "3.35.1"
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.35.0.tgz#03f2900698c7c547fce9fb8fb8d0b9a56c8da405" resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.35.1.tgz#297d15f534a3507cab2f5dfb996019cac7568f0c"
integrity sha512-Om4aLzkGbUi+Rc3sa8s48CRj2Qe7u5TXS7lK7Z681x2EiAa5Qx5uB/kbp8A6qY6dFDX7vstYRIYZ7t9XgdJ1dw== integrity sha512-K4yKVhaHkXfJ/xcUnil04xiSrB8B8yHZoFEhWNpXg23eiCnqvTZw1tn/SqvdsANlYHLJlKl0qi3I/Q2Sqo7LwQ==
dependencies: dependencies:
agentkeepalive "^2.2.0" agentkeepalive "^2.2.0"
debug "^2.6.9" debug "^2.6.9"
@ -1336,10 +1361,12 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
async@^1.5.2: async@^2.6.2:
version "1.5.2" version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
dependencies:
lodash "^4.17.14"
asynckit@^0.4.0: asynckit@^0.4.0:
version "0.4.0" version "0.4.0"
@ -1359,17 +1386,17 @@ autocomplete.js@0.36.0:
immediate "^3.2.3" immediate "^3.2.3"
autoprefixer@^9.4.5, autoprefixer@^9.5.1, autoprefixer@^9.6.1: autoprefixer@^9.4.5, autoprefixer@^9.5.1, autoprefixer@^9.6.1:
version "9.6.1" version "9.7.0"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.0.tgz#905ec19e50f04545fe9ff131182cc9ab25246901"
integrity sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw== integrity sha512-j2IRvaCfrUxIiZun9ba4mhJ2omhw4OY88/yVzLO+lHhGBumAAK72PgM6gkbSN8iregPOn1ZlxGkmZh2CQ7X4AQ==
dependencies: dependencies:
browserslist "^4.6.3" browserslist "^4.7.2"
caniuse-lite "^1.0.30000980" caniuse-lite "^1.0.30001004"
chalk "^2.4.2" chalk "^2.4.2"
normalize-range "^0.1.2" normalize-range "^0.1.2"
num2fraction "^1.2.2" num2fraction "^1.2.2"
postcss "^7.0.17" postcss "^7.0.19"
postcss-value-parser "^4.0.0" postcss-value-parser "^4.0.2"
aws-sign2@~0.7.0: aws-sign2@~0.7.0:
version "0.7.0" version "0.7.0"
@ -1381,13 +1408,6 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
babel-extract-comments@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz#0a2aedf81417ed391b85e18b4614e693a0351a21"
integrity sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ==
dependencies:
babylon "^6.18.0"
babel-loader@^8.0.4: babel-loader@^8.0.4:
version "8.0.6" version "8.0.6"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb"
@ -1416,32 +1436,6 @@ babel-plugin-module-resolver@3.2.0:
reselect "^3.0.1" reselect "^3.0.1"
resolve "^1.4.0" resolve "^1.4.0"
babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=
babel-plugin-transform-object-rest-spread@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
integrity sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=
dependencies:
babel-plugin-syntax-object-rest-spread "^6.8.0"
babel-runtime "^6.26.0"
babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
babylon@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -1493,9 +1487,9 @@ binary-extensions@^1.0.0:
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
bluebird@^3.1.1, bluebird@^3.5.5: bluebird@^3.1.1, bluebird@^3.5.5:
version "3.5.5" version "3.7.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de"
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8" version "4.11.8"
@ -1623,14 +1617,14 @@ browserify-zlib@^0.2.0:
dependencies: dependencies:
pako "~1.0.5" pako "~1.0.5"
browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.6.3: browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.7.2:
version "4.7.0" version "4.7.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.7.0.tgz#9ee89225ffc07db03409f2fee524dc8227458a17" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.7.2.tgz#1bb984531a476b5d389cedecb195b2cd69fb1348"
integrity sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA== integrity sha512-uZavT/gZXJd2UTi9Ov7/Z340WOSQ3+m1iBVRUknf+okKxonL9P83S3ctiBDtuRmRu8PiCHjqyueqQ9HYlJhxiw==
dependencies: dependencies:
caniuse-lite "^1.0.30000989" caniuse-lite "^1.0.30001004"
electron-to-chromium "^1.3.247" electron-to-chromium "^1.3.295"
node-releases "^1.1.29" node-releases "^1.1.38"
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.1" version "1.1.1"
@ -1801,10 +1795,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2" lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000989: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001004:
version "1.0.30000997" version "1.0.30001006"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000997.tgz#ba44a606804f8680894b7042612c2c7f65685b7e" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001006.tgz#5b6e8288792cfa275f007b2819a00ccad7112655"
integrity sha512-BQLFPIdj2ntgBNWp9Q64LGUIEmvhKkzzHhUHR3CD5A9Lb7ZKF20/+sgadhFap69lk5XmK1fTUleDclaRFvgVUA== integrity sha512-MXnUVX27aGs/QINz+QG1sWSLDr3P1A3Hq5EUWoIt0T7K24DuvMxZEnh3Y5aHlJW6Bz2aApJdSewdYLd8zQnUuw==
caseless@~0.12.0: caseless@~0.12.0:
version "0.12.0" version "0.12.0"
@ -1987,20 +1981,15 @@ commander@2.17.x:
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
commander@^2.20.0: commander@^2.20.0:
version "2.20.1" version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.1.tgz#3863ce3ca92d0831dcf2a102f5fb4b5926afd0f9" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@~2.19.0: commander@~2.19.0:
version "2.19.0" version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
comment-regex@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/comment-regex/-/comment-regex-1.0.1.tgz#e070d2c4db33231955d0979d27c918fcb6f93565"
integrity sha512-IWlN//Yfby92tOIje7J18HkNmWRR7JESA/BK8W7wqY/akITpU5B0JQWnbTjCfdChSrDNb0DrdA9jfAxiiBXyiQ==
commondir@^1.0.1: commondir@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@ -2057,11 +2046,9 @@ consola@^2.6.0:
integrity sha512-4sxpH6SGFYLADfUip4vuY65f/gEogrzJoniVhNUYkJHtng0l8ZjnDCqxxrSVRHOHwKxsy8Vm5ONZh1wOR3/l/w== integrity sha512-4sxpH6SGFYLADfUip4vuY65f/gEogrzJoniVhNUYkJHtng0l8ZjnDCqxxrSVRHOHwKxsy8Vm5ONZh1wOR3/l/w==
console-browserify@^1.1.0: console-browserify@^1.1.0:
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
dependencies:
date-now "^0.1.4"
console-control-strings@^1.0.0, console-control-strings@~1.1.0: console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0" version "1.1.0"
@ -2144,10 +2131,10 @@ copy-webpack-plugin@^5.0.2:
serialize-javascript "^1.7.0" serialize-javascript "^1.7.0"
webpack-log "^2.0.0" webpack-log "^2.0.0"
core-js@^2.4.0, core-js@^2.6.5: core-js@^2.6.5:
version "2.6.9" version "2.6.10"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f"
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==
core-util-is@1.0.2, core-util-is@~1.0.0: core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
@ -2285,21 +2272,13 @@ css-select@^2.0.0:
domutils "^1.7.0" domutils "^1.7.0"
nth-check "^1.0.2" nth-check "^1.0.2"
css-tree@1.0.0-alpha.29: css-tree@1.0.0-alpha.37:
version "1.0.0-alpha.29" version "1.0.0-alpha.37"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg== integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
dependencies:
mdn-data "~1.1.0"
source-map "^0.5.3"
css-tree@1.0.0-alpha.33:
version "1.0.0-alpha.33"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.33.tgz#970e20e5a91f7a378ddd0fc58d0b6c8d4f3be93e"
integrity sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==
dependencies: dependencies:
mdn-data "2.0.4" mdn-data "2.0.4"
source-map "^0.5.3" source-map "^0.6.1"
css-unit-converter@^1.1.1: css-unit-converter@^1.1.1:
version "1.1.1" version "1.1.1"
@ -2311,11 +2290,6 @@ css-what@2.1, css-what@^2.1.2:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
css.escape@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=
css@^2.0.0: css@^2.0.0:
version "2.2.4" version "2.2.4"
resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
@ -2404,12 +2378,12 @@ cssnano@^4.1.10:
is-resolvable "^1.0.0" is-resolvable "^1.0.0"
postcss "^7.0.0" postcss "^7.0.0"
csso@^3.5.1: csso@^4.0.2:
version "3.5.1" version "4.0.2"
resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.2.tgz#e5f81ab3a56b8eefb7f0092ce7279329f454de3d"
integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== integrity sha512-kS7/oeNVXkHWxby5tHVxlhjizRCSv8QdU7hB2FpdAibDU8FjTAolhNjKNTiLzXtUrKT6HwClE81yXwEk1309wg==
dependencies: dependencies:
css-tree "1.0.0-alpha.29" css-tree "1.0.0-alpha.37"
cyclist@^1.0.1: cyclist@^1.0.1:
version "1.0.1" version "1.0.1"
@ -2423,11 +2397,6 @@ dashdash@^1.12.0:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
de-indent@^1.0.2: de-indent@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@ -2440,7 +2409,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@^3.0.0, debug@^3.2.5, debug@^3.2.6: debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@ -2530,11 +2499,6 @@ define-property@^2.0.2:
is-descriptor "^1.0.2" is-descriptor "^1.0.2"
isobject "^3.0.1" isobject "^3.0.1"
defined@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
del@^4.1.1: del@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4"
@ -2733,10 +2697,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.247: electron-to-chromium@^1.3.295:
version "1.3.268" version "1.3.296"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.268.tgz#d18f10b064dd0fce39098704896309a8249cb62f" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.296.tgz#a1d4322d742317945285d3ba88966561b67f3ac8"
integrity sha512-QkPEya233zGh+1erw/N/GNgLjs+t65wkGX4Yw0X/ZuO75r+4Ropk7toXSUqP3TQ7EIwBDotTks3rbNZ1Kwz8hA== integrity sha512-s5hv+TSJSVRsxH190De66YHb50pBGTweT9XGWYu/LMR20KX6TsjFzObo36CjVAzM+PUeeKSBRtm/mISlCzeojQ==
elliptic@^6.0.0: elliptic@^6.0.0:
version "6.5.1" version "6.5.1"
@ -2774,12 +2738,12 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
once "^1.4.0" once "^1.4.0"
enhanced-resolve@^4.1.0: enhanced-resolve@^4.1.0:
version "4.1.0" version "4.1.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66"
integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==
dependencies: dependencies:
graceful-fs "^4.1.2" graceful-fs "^4.1.2"
memory-fs "^0.4.0" memory-fs "^0.5.0"
tapable "^1.0.0" tapable "^1.0.0"
entities@^1.1.1, entities@~1.1.1: entities@^1.1.1, entities@~1.1.1:
@ -2820,9 +2784,9 @@ error-ex@^1.3.1:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
es-abstract@^1.12.0, es-abstract@^1.5.1: es-abstract@^1.12.0, es-abstract@^1.5.1:
version "1.14.2" version "1.16.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.14.2.tgz#7ce108fad83068c8783c3cdf62e504e084d8c497" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d"
integrity sha512-DgoQmbpFNOofkjJtKwr87Ma5EW4Dc8fWhD0R+ndq7Oc456ivUfGOOP6oAZTTKl5/CcNMP+EN+e3/iUzgE0veZg== integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==
dependencies: dependencies:
es-to-primitive "^1.2.0" es-to-primitive "^1.2.0"
function-bind "^1.1.1" function-bind "^1.1.1"
@ -2832,8 +2796,8 @@ es-abstract@^1.12.0, es-abstract@^1.5.1:
is-regex "^1.0.4" is-regex "^1.0.4"
object-inspect "^1.6.0" object-inspect "^1.6.0"
object-keys "^1.1.1" object-keys "^1.1.1"
string.prototype.trimleft "^2.0.0" string.prototype.trimleft "^2.1.0"
string.prototype.trimright "^2.0.0" string.prototype.trimright "^2.1.0"
es-to-primitive@^1.2.0: es-to-primitive@^1.2.0:
version "1.2.0" version "1.2.0"
@ -3072,9 +3036,9 @@ figgy-pudding@^3.5.1:
integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==
figures@^3.0.0: figures@^3.0.0:
version "3.0.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.0.0.tgz#756275c964646163cc6f9197c7a0295dbfd04de9" resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec"
integrity sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g== integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==
dependencies: dependencies:
escape-string-regexp "^1.0.5" escape-string-regexp "^1.0.5"
@ -3204,15 +3168,6 @@ from2@^2.1.0:
inherits "^2.0.1" inherits "^2.0.1"
readable-stream "^2.0.0" readable-stream "^2.0.0"
fs-extra@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^7.0.1: fs-extra@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
@ -3266,11 +3221,6 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
gather-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gather-stream/-/gather-stream-1.0.0.tgz#b33994af457a8115700d410f317733cbe7a0904b"
integrity sha1-szmUr0V6gRVwDUEPMXczy+egkEs=
gauge@~2.7.3: gauge@~2.7.3:
version "2.7.4" version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@ -3328,9 +3278,9 @@ glob-to-regexp@^0.3.0:
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.4" version "7.1.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== integrity sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==
dependencies: dependencies:
fs.realpath "^1.0.0" fs.realpath "^1.0.0"
inflight "^1.0.4" inflight "^1.0.4"
@ -3397,9 +3347,9 @@ good-listener@^1.2.2:
delegate "^3.1.2" delegate "^3.1.2"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.2" version "4.2.3"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
gray-matter@^4.0.1: gray-matter@^4.0.1:
version "4.0.2" version "4.0.2"
@ -3436,11 +3386,6 @@ has-ansi@^2.0.0:
dependencies: dependencies:
ansi-regex "^2.0.0" ansi-regex "^2.0.0"
has-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=
has-flag@^3.0.0: has-flag@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@ -3644,7 +3589,7 @@ http-errors@~1.7.2:
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4"
integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=
http-proxy-middleware@^0.19.1: http-proxy-middleware@0.19.1:
version "0.19.1" version "0.19.1"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a"
integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==
@ -3707,9 +3652,9 @@ iferr@^0.1.5:
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
ignore-walk@^3.0.1: ignore-walk@^3.0.1:
version "3.0.2" version "3.0.3"
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.2.tgz#99d83a246c196ea5c93ef9315ad7b0819c35069b" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
integrity sha512-EXyErtpHbn75ZTsOADsfx6J/FPo6/5cjev46PXrcTpd8z3BoRkXgYu9/JVqrI7tusjmwCZutGeRJeU0Wo1e4Cw== integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
dependencies: dependencies:
minimatch "^3.0.4" minimatch "^3.0.4"
@ -3846,7 +3791,7 @@ is-absolute-url@^2.0.0:
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
is-absolute-url@^3.0.2: is-absolute-url@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
@ -4120,11 +4065,6 @@ javascript-stringify@^1.6.0:
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3" resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3"
integrity sha1-FC0RHzpuPa6PSpr9d9RYVbWpzOM= integrity sha1-FC0RHzpuPa6PSpr9d9RYVbWpzOM=
js-base64@^2.1.9:
version "2.5.1"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==
js-levenshtein@^1.1.3: js-levenshtein@^1.1.3:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
@ -4196,9 +4136,9 @@ json5@^1.0.1:
minimist "^1.2.0" minimist "^1.2.0"
json5@^2.1.0: json5@^2.1.0:
version "2.1.0" version "2.1.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6"
integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==
dependencies: dependencies:
minimist "^1.2.0" minimist "^1.2.0"
@ -4325,6 +4265,11 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.kebabcase@^4.1.1: lodash.kebabcase@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
@ -4350,11 +4295,6 @@ lodash.templatesettings@^4.0.0:
dependencies: dependencies:
lodash._reinterpolate "^3.0.0" lodash._reinterpolate "^3.0.0"
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
lodash.toarray@^4.4.0: lodash.toarray@^4.4.0:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
@ -4365,7 +4305,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5: lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5:
version "4.17.15" version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@ -4435,9 +4375,9 @@ map-visit@^1.0.0:
object-visit "^1.0.0" object-visit "^1.0.0"
markdown-it-anchor@^5.0.2: markdown-it-anchor@^5.0.2:
version "5.2.4" version "5.2.5"
resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.2.4.tgz#d39306fe4c199705b4479d3036842cf34dcba24f" resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.2.5.tgz#dbf13cfcdbffd16a510984f1263e1d479a47d27a"
integrity sha512-n8zCGjxA3T+Mx1pG8HEgbJbkB8JFUuRkeTZQuIM8iPY6oQ8sWOPRZJDFC9a/pNg2QkHEjjGkhBEl/RSyzaDZ3A== integrity sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ==
markdown-it-chain@^1.3.0: markdown-it-chain@^1.3.0:
version "1.3.0" version "1.3.0"
@ -4486,11 +4426,6 @@ mdn-data@2.0.4:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
mdn-data@~1.1.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01"
integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==
mdurl@^1.0.1: mdurl@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@ -4510,7 +4445,7 @@ mem@^4.0.0:
mimic-fn "^2.0.0" mimic-fn "^2.0.0"
p-is-promise "^2.0.0" p-is-promise "^2.0.0"
memory-fs@^0.4.0, memory-fs@^0.4.1: memory-fs@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
@ -4518,6 +4453,14 @@ memory-fs@^0.4.0, memory-fs@^0.4.1:
errno "^0.1.3" errno "^0.1.3"
readable-stream "^2.0.1" readable-stream "^2.0.1"
memory-fs@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==
dependencies:
errno "^0.1.3"
readable-stream "^2.0.1"
merge-descriptors@1.0.1: merge-descriptors@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@ -4643,15 +4586,7 @@ minimist@^1.2.0:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
minipass@^2.6.0, minipass@^2.8.6: minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
version "2.8.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.8.6.tgz#620d889ace26356391d010ecb9458749df9b6db5"
integrity sha512-lFG7d6g3+/UaFDCOtqPiKAC9zngWWsQZl1g5q6gaONqrjq61SX2xFqXMleQiFVyDpYwa018E9hmlAFY22PCb+A==
dependencies:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minipass@^2.9.0:
version "2.9.0" version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
@ -4660,9 +4595,9 @@ minipass@^2.9.0:
yallist "^3.0.0" yallist "^3.0.0"
minizlib@^1.2.1: minizlib@^1.2.1:
version "1.3.2" version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.2.tgz#5d24764998f98112586f7e566bd4c0999769dad4" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
integrity sha512-lsNFqSHdJ21EwKzCp12HHJGxSMtHkCW1EMA9cceG3MkMNARjuWotZnMe3NKNshAvFXpm4loZqmYsCmRwhS2JMw== integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
dependencies: dependencies:
minipass "^2.9.0" minipass "^2.9.0"
@ -4695,7 +4630,7 @@ mkdirp@0.3.0:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4= integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=
mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1, mkdirp@~0.5.x: mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1, mkdirp@~0.5.x:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
@ -4802,10 +4737,10 @@ node-emoji@^1.8.1:
dependencies: dependencies:
lodash.toarray "^4.4.0" lodash.toarray "^4.4.0"
node-forge@0.8.2: node-forge@0.9.0:
version "0.8.2" version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.8.2.tgz#b4bcc59fb12ce77a8825fc6a783dfe3182499c5a" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
integrity sha512-mXQ9GBq1N3uDCyV1pdSzgIguwgtVpM7f5/5J4ipz12PKWElmPpVWLDuWl8iXmhysr21+WmX/OJ5UKx82wjomgg== integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
node-libs-browser@^2.2.1: node-libs-browser@^2.2.1:
version "2.2.1" version "2.2.1"
@ -4852,12 +4787,12 @@ node-pre-gyp@^0.12.0:
semver "^5.3.0" semver "^5.3.0"
tar "^4" tar "^4"
node-releases@^1.1.29: node-releases@^1.1.38:
version "1.1.32" version "1.1.39"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.32.tgz#485b35c1bf9b4d8baa105d782f8ca731e518276e" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.39.tgz#c1011f30343aff5b633153b10ff691d278d08e8d"
integrity sha512-VhVknkitq8dqtWoluagsGPn3dxTvN9fwgR59fV3D7sLBHe0JfDramsMI8n8mY//ccq/Kkrf8ZRHRpsyVZ3qw1A== integrity sha512-8MRC/ErwNCHOlAFycy9OPca46fQYUjbJRDcZTHVWIGXIjYLM73k70vv3WkYutVnM4cCo4hE0MqBVVZjP6vjISA==
dependencies: dependencies:
semver "^5.3.0" semver "^6.3.0"
nopt@1.0.10: nopt@1.0.10:
version "1.0.10" version "1.0.10"
@ -4916,9 +4851,9 @@ npm-bundled@^1.0.1:
integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
npm-packlist@^1.1.6: npm-packlist@^1.1.6:
version "1.4.4" version "1.4.6"
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.6.tgz#53ba3ed11f8523079f1457376dd379ee4ea42ff4"
integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== integrity sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg==
dependencies: dependencies:
ignore-walk "^3.0.1" ignore-walk "^3.0.1"
npm-bundled "^1.0.1" npm-bundled "^1.0.1"
@ -5295,22 +5230,6 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
perfectionist@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/perfectionist/-/perfectionist-2.4.0.tgz#c147ad3714e126467f1764129ee72df861d47ea0"
integrity sha1-wUetNxThJkZ/F2QSnuct+GHUfqA=
dependencies:
comment-regex "^1.0.0"
defined "^1.0.0"
minimist "^1.2.0"
postcss "^5.0.8"
postcss-scss "^0.3.0"
postcss-value-parser "^3.3.0"
read-file-stdin "^0.2.0"
string.prototype.repeat "^0.2.0"
vendors "^1.0.0"
write-file-stdout "0.0.2"
performance-now@^2.1.0: performance-now@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@ -5357,14 +5276,14 @@ pkg-up@^2.0.0:
dependencies: dependencies:
find-up "^2.1.0" find-up "^2.1.0"
portfinder@^1.0.13, portfinder@^1.0.24: portfinder@^1.0.13, portfinder@^1.0.25:
version "1.0.24" version "1.0.25"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.24.tgz#11efbc6865f12f37624b6531ead1d809ed965cfa" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca"
integrity sha512-ekRl7zD2qxYndYflwiryJwMioBI7LI7rVXg3EnLK3sjkouT5eOuhS3gS255XxBksa30VG8UPZYZCdgfGOfkSUg== integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==
dependencies: dependencies:
async "^1.5.2" async "^2.6.2"
debug "^2.2.0" debug "^3.1.1"
mkdirp "0.5.x" mkdirp "^0.5.1"
posix-character-classes@^0.1.0: posix-character-classes@^0.1.0:
version "0.1.1" version "0.1.1"
@ -5693,13 +5612,6 @@ postcss-safe-parser@^4.0.1:
dependencies: dependencies:
postcss "^7.0.0" postcss "^7.0.0"
postcss-scss@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-0.3.1.tgz#65c610d8e2a7ee0e62b1835b71b8870734816e4b"
integrity sha1-ZcYQ2OKn7g5isYNbcbiHBzSBbks=
dependencies:
postcss "^5.2.4"
postcss-selector-parser@^3.0.0: postcss-selector-parser@^3.0.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
@ -5751,21 +5663,11 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3, postcss-value-parser@^
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
postcss-value-parser@^4.0.0: postcss-value-parser@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9"
integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==
postcss@^5.0.8, postcss@^5.2.4:
version "5.2.18"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5"
integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==
dependencies:
chalk "^1.1.3"
js-base64 "^2.1.9"
source-map "^0.5.6"
supports-color "^3.2.3"
postcss@^6.0.9: postcss@^6.0.9:
version "6.0.23" version "6.0.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
@ -5775,10 +5677,10 @@ postcss@^6.0.9:
source-map "^0.6.1" source-map "^0.6.1"
supports-color "^5.4.0" supports-color "^5.4.0"
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.18, postcss@^7.0.5, postcss@^7.0.6: postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.18, postcss@^7.0.19, postcss@^7.0.5, postcss@^7.0.6:
version "7.0.18" version "7.0.21"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.18.tgz#4b9cda95ae6c069c67a4d933029eddd4838ac233" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17"
integrity sha512-/7g1QXXgegpF+9GJj4iN7ChGF40sYuGYJ8WZu8DZWnmhQ/G36hfdk3q9LBJmoK+lZ+yzZ5KYpOoxq7LF1BxE8g== integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==
dependencies: dependencies:
chalk "^2.4.2" chalk "^2.4.2"
source-map "^0.6.1" source-map "^0.6.1"
@ -5915,9 +5817,9 @@ punycode@^2.1.0:
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
purgecss@^1.4.0: purgecss@^1.4.0:
version "1.4.0" version "1.4.1"
resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-1.4.0.tgz#79905624ec1c6c8e1f03044bca92dd8a598ba429" resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-1.4.1.tgz#d362e63eb1ed9dd1fa1554b9fd7accb8d54e56dc"
integrity sha512-or7/16i7O6DH+NpXqY8NCcWCc940O6PxOgjWAcMTElzgccKOJua1/n6JVtM8UYqoMMWoCyKk+CbLpo4+4mY3BQ== integrity sha512-5jONV/D/3nfa+lC425+LA+OWe5/LDn4a79cac+TnzJq3VczwnWlpIDdW275hHsGhkzIlqATQsYFLW7or0cSwNQ==
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
postcss "^7.0.14" postcss "^7.0.14"
@ -6010,13 +5912,6 @@ read-cache@^1.0.0:
dependencies: dependencies:
pify "^2.3.0" pify "^2.3.0"
read-file-stdin@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/read-file-stdin/-/read-file-stdin-0.2.1.tgz#25eccff3a153b6809afacb23ee15387db9e0ee61"
integrity sha1-JezP86FTtoCa+ssj7hU4fbng7mE=
dependencies:
gather-stream "^1.0.0"
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.6" version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
@ -6049,9 +5944,9 @@ readdirp@^2.2.1:
readable-stream "^2.0.2" readable-stream "^2.0.2"
reduce-css-calc@^2.1.6: reduce-css-calc@^2.1.6:
version "2.1.6" version "2.1.7"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.6.tgz#050fe6ee7d98a1d70775d2e93ce0b713cee394d2" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"
integrity sha512-+l5/qlQmdsbM9h6JerJ/y5vR5Ci0k93aszLNpCmbadC3nBcbRGmIBm0s9Nj59i22LvCjTGftWzdQRwdknayxhw== integrity sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==
dependencies: dependencies:
css-unit-converter "^1.1.1" css-unit-converter "^1.1.1"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
@ -6075,11 +5970,6 @@ regenerate@^1.4.0:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.2: regenerator-runtime@^0.13.2:
version "0.13.3" version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
@ -6120,9 +6010,9 @@ regexpu-core@^4.6.0:
unicode-match-property-value-ecmascript "^1.1.0" unicode-match-property-value-ecmascript "^1.1.0"
regjsgen@^0.5.0: regjsgen@^0.5.0:
version "0.5.0" version "0.5.1"
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c"
integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA== integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==
regjsparser@^0.6.0: regjsparser@^0.6.0:
version "0.6.0" version "0.6.0"
@ -6333,12 +6223,12 @@ select@^1.1.2:
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
selfsigned@^1.10.6: selfsigned@^1.10.7:
version "1.10.6" version "1.10.7"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.6.tgz#7b3cd37ed9c2034261a173af1a1aae27d8169b67" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b"
integrity sha512-i3+CeqxL7DpAazgVpAGdKMwHuL63B5nhJMh9NQ7xmChGkA3jNFflq6Jyo1LLJYcr3idWiNOPWHCrm4zMayLG4w== integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==
dependencies: dependencies:
node-forge "0.8.2" node-forge "0.9.0"
semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.7.1" version "5.7.1"
@ -6469,6 +6359,11 @@ slash@^2.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
smoothscroll-polyfill@^0.4.3:
version "0.4.4"
resolved "https://registry.yarnpkg.com/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz#3a259131dc6930e6ca80003e1cb03b603b69abf8"
integrity sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==
snapdragon-node@^2.0.1: snapdragon-node@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -6543,9 +6438,9 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
urix "^0.1.0" urix "^0.1.0"
source-map-support@~0.5.12: source-map-support@~0.5.12:
version "0.5.13" version "0.5.16"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
dependencies: dependencies:
buffer-from "^1.0.0" buffer-from "^1.0.0"
source-map "^0.6.0" source-map "^0.6.0"
@ -6560,7 +6455,7 @@ source-map@0.5.6:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6: source-map@^0.5.0, source-map@^0.5.6:
version "0.5.7" version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@ -6725,12 +6620,7 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0" strip-ansi "^5.1.0"
string.prototype.repeat@^0.2.0: string.prototype.trimleft@^2.1.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz#aba36de08dcee6a5a337d49b2ea1da1b28fc0ecf"
integrity sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=
string.prototype.trimleft@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
@ -6738,7 +6628,7 @@ string.prototype.trimleft@^2.0.0:
define-properties "^1.1.3" define-properties "^1.1.3"
function-bind "^1.1.1" function-bind "^1.1.1"
string.prototype.trimright@^2.0.0: string.prototype.trimright@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
@ -6786,14 +6676,6 @@ strip-bom-string@^1.0.0:
resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
integrity sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI= integrity sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=
strip-comments@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-1.0.2.tgz#82b9c45e7f05873bee53f37168af930aa368679d"
integrity sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw==
dependencies:
babel-extract-comments "^1.0.0"
babel-plugin-transform-object-rest-spread "^6.26.0"
strip-eof@^1.0.0: strip-eof@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@ -6841,13 +6723,6 @@ supports-color@^2.0.0:
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
supports-color@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=
dependencies:
has-flag "^1.0.0"
supports-color@^5.3.0, supports-color@^5.4.0: supports-color@^5.3.0, supports-color@^5.4.0:
version "5.5.0" version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@ -6868,16 +6743,16 @@ svg-tags@^1.0.0:
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
svgo@^1.0.0: svgo@^1.0.0:
version "1.3.0" version "1.3.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.0.tgz#bae51ba95ded9a33a36b7c46ce9c359ae9154313" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
integrity sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ== integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
dependencies: dependencies:
chalk "^2.4.1" chalk "^2.4.1"
coa "^2.0.2" coa "^2.0.2"
css-select "^2.0.0" css-select "^2.0.0"
css-select-base-adapter "^0.1.1" css-select-base-adapter "^0.1.1"
css-tree "1.0.0-alpha.33" css-tree "1.0.0-alpha.37"
csso "^3.5.1" csso "^4.0.2"
js-yaml "^3.13.1" js-yaml "^3.13.1"
mkdirp "~0.5.1" mkdirp "~0.5.1"
object.values "^1.1.0" object.values "^1.1.0"
@ -6886,31 +6761,10 @@ svgo@^1.0.0:
unquote "~1.1.1" unquote "~1.1.1"
util.promisify "~1.0.0" util.promisify "~1.0.0"
tailwindcss@^0.7.4: tailwindcss@^1.0.6, tailwindcss@^1.1.2:
version "0.7.4" version "1.1.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-0.7.4.tgz#fb7926821d42eacdc12e6621a49d21f37a3ff9e9" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.1.3.tgz#ad154f78e1e44060e32e3ed44b27287c2be126a6"
integrity sha512-+GeQjHRJ2VmeLkrNwMCbPDfm2cc5P8eoc7n+DtZfI8oQdlo5eSHqsIlPEuZOtoqQlIALsd2jAggWrUUBFGP2ow== integrity sha512-8sa/QO+blnu3WXUylsgvYZlUbBpVH36QeGuZxgSGqp1dF3g4AGe1azt8FsO8i8Hfe9Oyvwhx3iSjRDak3nngeQ==
dependencies:
autoprefixer "^9.4.5"
bytes "^3.0.0"
chalk "^2.4.1"
css.escape "^1.5.1"
fs-extra "^4.0.2"
lodash "^4.17.5"
node-emoji "^1.8.1"
perfectionist "^2.4.0"
postcss "^7.0.11"
postcss-functions "^3.0.0"
postcss-js "^2.0.0"
postcss-nested "^4.1.1"
postcss-selector-parser "^5.0.0"
pretty-hrtime "^1.0.3"
strip-comments "^1.0.2"
tailwindcss@^1.0.6:
version "1.1.2"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.1.2.tgz#0107dc092c3edee6132b105d896b109c0f66afd6"
integrity sha512-mcTzZHXMipnQY9haB17baNJmBTkYYcC8ljfMdB9/97FfhKJIzlglJcyGythuQTOu7r/QIbLfZYYWZhAvaGj95A==
dependencies: dependencies:
autoprefixer "^9.4.5" autoprefixer "^9.4.5"
bytes "^3.0.0" bytes "^3.0.0"
@ -6961,9 +6815,9 @@ terser-webpack-plugin@^1.4.1:
worker-farm "^1.7.0" worker-farm "^1.7.0"
terser@^4.1.2: terser@^4.1.2:
version "4.3.4" version "4.3.9"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.4.tgz#ad91bade95619e3434685d69efa621a5af5f877d" resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.9.tgz#e4be37f80553d02645668727777687dad26bbca8"
integrity sha512-Kcrn3RiW8NtHBP0ssOAzwa2MsIRQ8lJWiBG/K7JgqPlomA3mtb2DEmp4/hrUA+Jujx+WZ02zqd7GYD+QRBB/2Q== integrity sha512-NFGMpHjlzmyOtPL+fDw3G7+6Ueh/sz4mkaUYa4lJCxOPTNzd0Uj0aZJOmsDYoSQyfuVoWDMSWTPU3huyOm2zdA==
dependencies: dependencies:
commander "^2.20.0" commander "^2.20.0"
source-map "~0.6.1" source-map "~0.6.1"
@ -6988,9 +6842,9 @@ through@~2.3.4:
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
thunky@^1.0.2: thunky@^1.0.2:
version "1.0.3" version "1.1.0"
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow== integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
timers-browserify@^2.0.4: timers-browserify@^2.0.4:
version "2.0.11" version "2.0.11"
@ -7328,7 +7182,7 @@ vue-hot-reload-api@^2.3.0:
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog== integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
vue-loader@^15.2.4: vue-loader@^15.7.1:
version "15.7.1" version "15.7.1"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.7.1.tgz#6ccacd4122aa80f69baaac08ff295a62e3aefcfd" resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.7.1.tgz#6ccacd4122aa80f69baaac08ff295a62e3aefcfd"
integrity sha512-fwIKtA23Pl/rqfYP5TSGK7gkEuLhoTvRYW+TU7ER3q9GpNLt/PjG5NLv3XHRDiTg7OPM1JcckBgds+VnAc+HbA== integrity sha512-fwIKtA23Pl/rqfYP5TSGK7gkEuLhoTvRYW+TU7ER3q9GpNLt/PjG5NLv3XHRDiTg7OPM1JcckBgds+VnAc+HbA==
@ -7339,12 +7193,12 @@ vue-loader@^15.2.4:
vue-hot-reload-api "^2.3.0" vue-hot-reload-api "^2.3.0"
vue-style-loader "^4.1.0" vue-style-loader "^4.1.0"
vue-router@^3.0.2: vue-router@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.1.3.tgz#e6b14fabc0c0ee9fda0e2cbbda74b350e28e412b" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.1.3.tgz#e6b14fabc0c0ee9fda0e2cbbda74b350e28e412b"
integrity sha512-8iSa4mGNXBjyuSZFCCO4fiKfvzqk+mhL0lnKuGcQtO1eoj8nq3CmbEG8FwK5QqoqwDgsjsf1GDuisDX4cdb/aQ== integrity sha512-8iSa4mGNXBjyuSZFCCO4fiKfvzqk+mhL0lnKuGcQtO1eoj8nq3CmbEG8FwK5QqoqwDgsjsf1GDuisDX4cdb/aQ==
vue-server-renderer@^2.5.16: vue-server-renderer@^2.6.10:
version "2.6.10" version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.6.10.tgz#cb2558842ead360ae2ec1f3719b75564a805b375" resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.6.10.tgz#cb2558842ead360ae2ec1f3719b75564a805b375"
integrity sha512-UYoCEutBpKzL2fKCwx8zlRtRtwxbPZXKTqbl2iIF4yRZUNO/ovrHyDAJDljft0kd+K0tZhN53XRHkgvCZoIhug== integrity sha512-UYoCEutBpKzL2fKCwx8zlRtRtwxbPZXKTqbl2iIF4yRZUNO/ovrHyDAJDljft0kd+K0tZhN53XRHkgvCZoIhug==
@ -7366,7 +7220,7 @@ vue-style-loader@^4.1.0:
hash-sum "^1.0.2" hash-sum "^1.0.2"
loader-utils "^1.0.2" loader-utils "^1.0.2"
vue-template-compiler@^2.5.16: vue-template-compiler@^2.6.10:
version "2.6.10" version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg== integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
@ -7379,7 +7233,7 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue@^2.5.16: vue@^2.6.10:
version "2.6.10" version "2.6.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ== integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==
@ -7397,20 +7251,27 @@ vuepress-html-webpack-plugin@^3.2.0:
toposort "^1.0.0" toposort "^1.0.0"
util.promisify "1.0.0" util.promisify "1.0.0"
vuepress-plugin-container@^2.0.0: vuepress-plugin-container@^2.0.2:
version "2.0.2" version "2.1.0"
resolved "https://registry.yarnpkg.com/vuepress-plugin-container/-/vuepress-plugin-container-2.0.2.tgz#3489cc732c7a210b31f202556e1346125dffeb73" resolved "https://registry.yarnpkg.com/vuepress-plugin-container/-/vuepress-plugin-container-2.1.0.tgz#eb2ba3e01cdac419bd678d40e05c934caffe6db0"
integrity sha512-SrGYYT7lkie7xlIlAVhn+9sDW42MytNCoxWL/2uDr+q9wZA4h1uYlQvfc2DVjy+FsM9PPPSslkeo/zCpYVY82g== integrity sha512-i4p7S1cqYUrg/3pt+xSghZtKSHVI3VXMQNept8ILxA+lMK1XJkdRkjNovZzwpXlrErQssvrUOTWBV0hdBv7eXQ==
dependencies: dependencies:
markdown-it-container "^2.0.0" markdown-it-container "^2.0.0"
vuepress@^1.0.0: vuepress-plugin-smooth-scroll@^0.0.3:
version "1.1.0" version "0.0.3"
resolved "https://registry.yarnpkg.com/vuepress/-/vuepress-1.1.0.tgz#ca0d787d93188b2fd05820a650d7e3643c9e7675" resolved "https://registry.yarnpkg.com/vuepress-plugin-smooth-scroll/-/vuepress-plugin-smooth-scroll-0.0.3.tgz#6eff2d4c186cca917cc9f7df2b0af7de7c8c6438"
integrity sha512-LAgS9nXsmvjTuCc/LHPWnIsPOuVuZtxh1MjVZf/xJ3Yy5kXoPhqbGUptlQdQt3izjIlns9zin5K6MNBY3u5l5g== integrity sha512-qsQkDftLVFLe8BiviIHaLV0Ea38YLZKKonDGsNQy1IE0wllFpFIEldWD8frWZtDFdx6b/O3KDMgVQ0qp5NjJCg==
dependencies: dependencies:
"@vuepress/core" "^1.1.0" smoothscroll-polyfill "^0.4.3"
"@vuepress/theme-default" "^1.1.0"
vuepress@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/vuepress/-/vuepress-1.2.0.tgz#2f2cdf337ad40a3e4866dfd33e97b840db386af7"
integrity sha512-EfHo8Cc73qo+1Pm18hM0qOGynmDr8q5fu2664obynsdCJ1zpvoShVnA0Msraw4SI2xDc0iAoIb3dTwxUIM8DAw==
dependencies:
"@vuepress/core" "^1.2.0"
"@vuepress/theme-default" "^1.2.0"
cac "^6.3.9" cac "^6.3.9"
envinfo "^7.2.0" envinfo "^7.2.0"
opencollective-postinstall "^2.0.2" opencollective-postinstall "^2.0.2"
@ -7449,7 +7310,7 @@ webpack-dev-middleware@3.6.0:
range-parser "^1.0.3" range-parser "^1.0.3"
webpack-log "^2.0.0" webpack-log "^2.0.0"
webpack-dev-middleware@^3.7.1: webpack-dev-middleware@^3.7.2:
version "3.7.2" version "3.7.2"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3"
integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==
@ -7461,9 +7322,9 @@ webpack-dev-middleware@^3.7.1:
webpack-log "^2.0.0" webpack-log "^2.0.0"
webpack-dev-server@^3.5.1: webpack-dev-server@^3.5.1:
version "3.8.1" version "3.9.0"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.8.1.tgz#485b64c4aadc23f601e72114b40c1b1fea31d9f1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.9.0.tgz#27c3b5d0f6b6677c4304465ac817623c8b27b89c"
integrity sha512-9F5DnfFA9bsrhpUCAfQic/AXBVHvq+3gQS+x6Zj0yc1fVVE0erKh2MV4IV12TBewuTrYeeTIRwCH9qLMvdNvTw== integrity sha512-E6uQ4kRrTX9URN9s/lIbqTAztwEPdvzVrcmHE8EQ9YnuT9J8Es5Wrd8n9BKg1a0oZ5EgEke/EQFgUsp18dSTBw==
dependencies: dependencies:
ansi-html "0.0.7" ansi-html "0.0.7"
bonjour "^3.5.0" bonjour "^3.5.0"
@ -7474,18 +7335,18 @@ webpack-dev-server@^3.5.1:
del "^4.1.1" del "^4.1.1"
express "^4.17.1" express "^4.17.1"
html-entities "^1.2.1" html-entities "^1.2.1"
http-proxy-middleware "^0.19.1" http-proxy-middleware "0.19.1"
import-local "^2.0.0" import-local "^2.0.0"
internal-ip "^4.3.0" internal-ip "^4.3.0"
ip "^1.1.5" ip "^1.1.5"
is-absolute-url "^3.0.2" is-absolute-url "^3.0.3"
killable "^1.0.1" killable "^1.0.1"
loglevel "^1.6.4" loglevel "^1.6.4"
opn "^5.5.0" opn "^5.5.0"
p-retry "^3.0.1" p-retry "^3.0.1"
portfinder "^1.0.24" portfinder "^1.0.25"
schema-utils "^1.0.0" schema-utils "^1.0.0"
selfsigned "^1.10.6" selfsigned "^1.10.7"
semver "^6.3.0" semver "^6.3.0"
serve-index "^1.9.1" serve-index "^1.9.1"
sockjs "0.3.19" sockjs "0.3.19"
@ -7494,7 +7355,7 @@ webpack-dev-server@^3.5.1:
strip-ansi "^3.0.1" strip-ansi "^3.0.1"
supports-color "^6.1.0" supports-color "^6.1.0"
url "^0.11.0" url "^0.11.0"
webpack-dev-middleware "^3.7.1" webpack-dev-middleware "^3.7.2"
webpack-log "^2.0.0" webpack-log "^2.0.0"
ws "^6.2.1" ws "^6.2.1"
yargs "12.0.5" yargs "12.0.5"
@ -7523,9 +7384,9 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1:
source-map "~0.6.1" source-map "~0.6.1"
webpack@^4.8.1: webpack@^4.8.1:
version "4.41.0" version "4.41.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.0.tgz#db6a254bde671769f7c14e90a1a55e73602fc70b" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.2.tgz#c34ec76daa3a8468c9b61a50336d8e3303dce74e"
integrity sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g== integrity sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==
dependencies: dependencies:
"@webassemblyjs/ast" "1.8.5" "@webassemblyjs/ast" "1.8.5"
"@webassemblyjs/helper-module-context" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5"
@ -7632,11 +7493,6 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
write-file-stdout@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/write-file-stdout/-/write-file-stdout-0.0.2.tgz#c252d7c7c5b1b402897630e3453c7bfe690d9ca1"
integrity sha1-wlLXx8WxtAKJdjDjRTx7/mkNnKE=
ws@^6.2.1: ws@^6.2.1:
version "6.2.1" version "6.2.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
@ -7660,9 +7516,9 @@ yallist@^2.1.2:
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
version "3.1.0" version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.0.tgz#906cc2100972dc2625ae78f566a2577230a1d6f7" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-6gpP93MR+VOOehKbCPchro3wFZNSNmek8A2kbkOAZLIZAYx1KP/zAqwO0sOHi3xJEb+UBz8NaYt/17UNit1Q9w== integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yargs-parser@^11.1.1: yargs-parser@^11.1.1:
version "11.1.1" version "11.1.1"
@ -7672,10 +7528,10 @@ yargs-parser@^11.1.1:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
yargs-parser@^13.1.1: yargs-parser@^15.0.0:
version "13.1.1" version "15.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.0.tgz#cdd7a97490ec836195f59f3f4dbe5ea9e8f75f08"
integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== integrity sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==
dependencies: dependencies:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
@ -7699,9 +7555,9 @@ yargs@12.0.5:
yargs-parser "^11.1.1" yargs-parser "^11.1.1"
yargs@^14.0.0: yargs@^14.0.0:
version "14.0.0" version "14.2.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.0.0.tgz#ba4cacc802b3c0b3e36a9e791723763d57a85066" resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.0.tgz#f116a9242c4ed8668790b40759b4906c276e76c3"
integrity sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow== integrity sha512-/is78VKbKs70bVZH7w4YaZea6xcJWOAwkhbR0CFuZBmYtfTYF0xjGJF43AYd8g2Uii1yJwmS5GR2vBmrc32sbg==
dependencies: dependencies:
cliui "^5.0.0" cliui "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
@ -7713,7 +7569,7 @@ yargs@^14.0.0:
string-width "^3.0.0" string-width "^3.0.0"
which-module "^2.0.0" which-module "^2.0.0"
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^13.1.1" yargs-parser "^15.0.0"
zepto@^1.2.0: zepto@^1.2.0:
version "1.2.0" version "1.2.0"

View File

@ -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))
} }

View File

@ -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)

View File

@ -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)
@ -214,6 +221,7 @@ func (c *compilerContext) renderDelete(qc *qcode.QCode, w io.Writer,
func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer, func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer,
vars Variables, ti *DBTableInfo) (uint32, error) { vars Variables, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0]
upsert, ok := vars[qc.ActionVar] upsert, ok := vars[qc.ActionVar]
if !ok { if !ok {
@ -229,7 +237,7 @@ func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer,
return 0, err return 0, err
} }
io.WriteString(c.w, ` ON CONFLICT DO (`) io.WriteString(c.w, ` ON CONFLICT (`)
i := 0 i := 0
for _, cn := range ti.ColumnNames { for _, cn := range ti.ColumnNames {
@ -250,10 +258,17 @@ func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer,
if i == 0 { if i == 0 {
io.WriteString(c.w, ti.PrimaryCol) io.WriteString(c.w, ti.PrimaryCol)
} }
io.WriteString(c.w, `) DO `) io.WriteString(c.w, `)`)
io.WriteString(c.w, `UPDATE `) if root.Where != nil {
io.WriteString(c.w, ` SET `) io.WriteString(c.w, ` WHERE `)
if err := c.renderWhere(root, ti); err != nil {
return 0, err
}
}
io.WriteString(c.w, ` DO UPDATE SET `)
i = 0 i = 0
for _, cn := range ti.ColumnNames { for _, cn := range ti.ColumnNames {

View File

@ -2,7 +2,6 @@ package psql
import ( import (
"encoding/json" "encoding/json"
"fmt"
"testing" "testing"
) )
@ -13,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"}`),
@ -37,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 }`),
@ -61,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" }]`),
@ -79,13 +78,37 @@ func bulkInsert(t *testing.T) {
func singleUpsert(t *testing.T) { func singleUpsert(t *testing.T) {
gql := `mutation { gql := `mutation {
product(id: 15, upsert: $upsert) { product(upsert: $upsert) {
id id
name name
} }
}` }`
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 DO (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{
"upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func singleUpsertWhere(t *testing.T) {
gql := `mutation {
product(upsert: $upsert, where: { price : { gt: 3 } }) {
id
name
}
}`
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" }`),
@ -103,13 +126,13 @@ func singleUpsert(t *testing.T) {
func bulkUpsert(t *testing.T) { func bulkUpsert(t *testing.T) {
gql := `mutation { gql := `mutation {
product(id: 15, upsert: $upsert) { product(upsert: $upsert) {
id id
name name
} }
}` }`
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 DO (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" }]`),
@ -133,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" }`),
@ -157,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" }`),
@ -180,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"}`),
@ -204,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"}`),
@ -227,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}`),
@ -250,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}`),
@ -261,8 +284,6 @@ func simpleUpdateWithPresets(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fmt.Println(string(resSQL))
if string(resSQL) != sql { if string(resSQL) != sql {
t.Fatal(errNotExpected) t.Fatal(errNotExpected)
} }
@ -274,6 +295,7 @@ func TestCompileMutate(t *testing.T) {
t.Run("bulkInsert", bulkInsert) t.Run("bulkInsert", bulkInsert)
t.Run("singleUpdate", singleUpdate) t.Run("singleUpdate", singleUpdate)
t.Run("singleUpsert", singleUpsert) t.Run("singleUpsert", singleUpsert)
t.Run("singleUpsertWhere", singleUpsertWhere)
t.Run("bulkUpsert", bulkUpsert) t.Run("bulkUpsert", bulkUpsert)
t.Run("delete", delete) t.Run("delete", delete)
t.Run("blockedInsert", blockedInsert) t.Run("blockedInsert", blockedInsert)

View File

@ -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{

View File

@ -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()
if multiRoot {
io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `)
for i, id := range qc.Roots {
root := qc.Selects[id]
st.Push(root.ID + closeBlock) st.Push(root.ID + closeBlock)
st.Push(root.ID) st.Push(root.ID)
//fmt.Fprintf(w, `SELECT json_object_agg('%s', %s) FROM (`, if i != 0 {
//root.FieldName, root.Table) io.WriteString(c.w, `, `)
io.WriteString(c.w, `SELECT json_object_agg('`) }
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 `)
} else {
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, root.FieldName)
io.WriteString(c.w, `', `) io.WriteString(c.w, `', `)
io.WriteString(c.w, `json_`)
if ti.Singular == false {
io.WriteString(c.w, root.Table)
} else {
io.WriteString(c.w, "sel_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.renderJoin(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.renderJoinClose(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, `)`) }
alias(c.w, `done_1337`) }
if multiRoot {
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,20 +352,20 @@ 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
} }
func (c *compilerContext) renderJoin(sel *qcode.Select) error { func (c *compilerContext) renderLateralJoin(sel *qcode.Select) error {
io.WriteString(c.w, ` LEFT OUTER JOIN LATERAL (`) io.WriteString(c.w, ` LEFT OUTER JOIN LATERAL (`)
return nil return nil
} }
func (c *compilerContext) renderJoinClose(sel *qcode.Select) error { func (c *compilerContext) renderLateralJoinClose(sel *qcode.Select) error {
//fmt.Fprintf(w, `) AS "%s_%d_join" ON ('true')`, c.sel.Table, c.sel.ID) //fmt.Fprintf(w, `) AS "%s_%d_join" ON ('true')`, c.sel.Table, c.sel.ID)
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
aliasWithIDSuffix(c.w, sel.Table, sel.ID, "_join") aliasWithIDSuffix(c.w, sel.Table, sel.ID, "_join")
@ -340,19 +373,24 @@ func (c *compilerContext) renderJoinClose(sel *qcode.Select) error {
return nil return nil
} }
func (c *compilerContext) renderJoinTable(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(ti.Name, parent.Table, parent.ID)
}
rel, err := c.schema.GetRel(sel.Table, parent.Table) func (c *compilerContext) renderJoinByName(table, parent string, id int32) error {
rel, err := c.schema.GetRel(table, parent)
if err != nil { if err != nil {
return err return err
} }
// This join is only required for one-to-many relations since
// these make use of join tables that need to be pulled in.
if rel.Type != RelOneToManyThrough { if rel.Type != RelOneToManyThrough {
return err return err
} }
pt, err := c.schema.GetTable(parent.Table) pt, err := c.schema.GetTable(parent)
if err != nil { if err != nil {
return err return err
} }
@ -364,7 +402,11 @@ func (c *compilerContext) renderJoinTable(sel *qcode.Select) error {
io.WriteString(c.w, `" ON ((`) io.WriteString(c.w, `" ON ((`)
colWithTable(c.w, rel.Through, rel.ColT) colWithTable(c.w, rel.Through, rel.ColT)
io.WriteString(c.w, `) = (`) io.WriteString(c.w, `) = (`)
colWithTableID(c.w, pt.Name, parent.ID, rel.Col1) if id != -1 {
colWithTableID(c.w, pt.Name, id, rel.Col1)
} else {
colWithTable(c.w, pt.Name, rel.Col1)
}
io.WriteString(c.w, `))`) io.WriteString(c.w, `))`)
return nil return nil
@ -436,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 {
continue
}
//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, `"`)
io.WriteString(c.w, childSel.Table)
io.WriteString(c.w, `_`)
int2string(c.w, childSel.ID)
io.WriteString(c.w, `_join"."json_`)
int2string(c.w, childSel.ID) int2string(c.w, childSel.ID)
io.WriteString(c.w, `" AS "`) io.WriteString(c.w, `" AS "`)
io.WriteString(c.w, childSel.FieldName) io.WriteString(c.w, childSel.FieldName)
io.WriteString(c.w, `"`) io.WriteString(c.w, `"`)
} else {
colWithTableIDSuffixAlias(c.w, childSel.Table, childSel.ID,
"_join", childSel.Table, childSel.FieldName)
}
} }
return nil return nil
@ -463,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
@ -529,9 +565,11 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
} else if sel.Functions { } else if sel.Functions {
cn1 := cn[pl:] cn1 := cn[pl:]
if len(sel.Allowed) != 0 {
if _, ok := sel.Allowed[cn1]; !ok { if _, ok := sel.Allowed[cn1]; !ok {
continue continue
} }
}
if i != 0 { if i != 0 {
io.WriteString(c.w, `, `) io.WriteString(c.w, `, `)
} }
@ -596,7 +634,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
} }
if !isRoot { if !isRoot {
if err := c.renderJoinTable(sel); err != nil { if err := c.renderJoin(sel, ti); err != nil {
return err return err
} }
@ -630,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)
@ -639,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`)
@ -680,8 +718,11 @@ 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(ti.Name, parent.Table, parent.ID)
}
rel, err := c.schema.GetRel(sel.Table, parent.Table) func (c *compilerContext) renderRelationshipByName(table, parent string, id int32) error {
rel, err := c.schema.GetRel(table, parent)
if err != nil { if err != nil {
return err return err
} }
@ -691,25 +732,34 @@ func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo)
//fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`, //fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`,
//c.sel.Table, rel.Col1, c.parent.Table, c.parent.ID, rel.Col2) //c.sel.Table, rel.Col1, c.parent.Table, c.parent.ID, rel.Col2)
io.WriteString(c.w, `((`) io.WriteString(c.w, `((`)
colWithTable(c.w, ti.Name, rel.Col1) colWithTable(c.w, table, rel.Col1)
io.WriteString(c.w, `) = (`) io.WriteString(c.w, `) = (`)
colWithTableID(c.w, parent.Table, parent.ID, rel.Col2) if id != -1 {
colWithTableID(c.w, parent, id, rel.Col2)
} else {
colWithTable(c.w, parent, rel.Col2)
}
io.WriteString(c.w, `))`) io.WriteString(c.w, `))`)
case RelOneToMany: case RelOneToMany:
//fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`, //fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`,
//c.sel.Table, rel.Col1, c.parent.Table, c.parent.ID, rel.Col2) //c.sel.Table, rel.Col1, c.parent.Table, c.parent.ID, rel.Col2)
io.WriteString(c.w, `((`) io.WriteString(c.w, `((`)
colWithTable(c.w, ti.Name, rel.Col1) colWithTable(c.w, table, rel.Col1)
io.WriteString(c.w, `) = (`) io.WriteString(c.w, `) = (`)
colWithTableID(c.w, parent.Table, parent.ID, rel.Col2) if id != -1 {
colWithTableID(c.w, parent, id, rel.Col2)
} else {
colWithTable(c.w, parent, rel.Col2)
}
io.WriteString(c.w, `))`) io.WriteString(c.w, `))`)
case RelOneToManyThrough: case RelOneToManyThrough:
// This requires the through table to be joined onto this select
//fmt.Fprintf(w, `(("%s"."%s") = ("%s"."%s"))`, //fmt.Fprintf(w, `(("%s"."%s") = ("%s"."%s"))`,
//c.sel.Table, rel.Col1, rel.Through, rel.Col2) //c.sel.Table, rel.Col1, rel.Through, rel.Col2)
io.WriteString(c.w, `((`) io.WriteString(c.w, `((`)
colWithTable(c.w, ti.Name, rel.Col1) colWithTable(c.w, table, rel.Col1)
io.WriteString(c.w, `) = (`) io.WriteString(c.w, `) = (`)
colWithTable(c.w, rel.Through, rel.Col2) colWithTable(c.w, rel.Through, rel.Col2)
io.WriteString(c.w, `))`) io.WriteString(c.w, `))`)
@ -768,21 +818,83 @@ func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error
qcode.FreeExp(val) qcode.FreeExp(val)
default: default:
if val.NestedCol { if len(val.NestedCols) != 0 {
//fmt.Fprintf(w, `(("%s") `, val.Col) io.WriteString(c.w, `EXISTS `)
io.WriteString(c.w, `(("`)
io.WriteString(c.w, val.Col)
io.WriteString(c.w, `") `)
} else if len(val.Col) != 0 { if err := c.renderNestedWhere(val, sel, ti); err != nil {
return err
}
} else {
//fmt.Fprintf(w, `(("%s"."%s") `, c.sel.Table, val.Col) //fmt.Fprintf(w, `(("%s"."%s") `, c.sel.Table, val.Col)
if err := c.renderOp(val, sel, ti); err != nil {
return err
}
qcode.FreeExp(val)
}
}
default:
return fmt.Errorf("12: unexpected value %v (%t)", intf, intf)
}
}
return nil
}
func (c *compilerContext) renderNestedWhere(ex *qcode.Exp, sel *qcode.Select, ti *DBTableInfo) error {
for i := 0; i < len(ex.NestedCols)-1; i++ {
cti, err := c.schema.GetTable(ex.NestedCols[i])
if err != nil {
return err
}
if i != 0 {
io.WriteString(c.w, ` AND `)
}
io.WriteString(c.w, `(SELECT 1 FROM `)
io.WriteString(c.w, cti.Name)
if err := c.renderJoinByName(cti.Name, ti.Name, -1); err != nil {
return err
}
io.WriteString(c.w, ` WHERE `)
if err := c.renderRelationshipByName(cti.Name, ti.Name, -1); err != nil {
return err
}
}
for i := 0; i < len(ex.NestedCols)-1; i++ {
io.WriteString(c.w, `)`)
}
return nil
}
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 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, val.Col) colWithTable(c.w, ti.Name, ex.Col)
io.WriteString(c.w, `) `) io.WriteString(c.w, `) `)
} }
valExists := true
switch val.Op { switch ex.Op {
case qcode.OpEquals: case qcode.OpEquals:
io.WriteString(c.w, `=`) io.WriteString(c.w, `=`)
case qcode.OpNotEquals: case qcode.OpNotEquals:
@ -822,16 +934,19 @@ func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error
case qcode.OpHasKeyAll: case qcode.OpHasKeyAll:
io.WriteString(c.w, `?&`) io.WriteString(c.w, `?&`)
case qcode.OpIsNull: case qcode.OpIsNull:
if strings.EqualFold(val.Val, "true") { if strings.EqualFold(ex.Val, "true") {
io.WriteString(c.w, `IS NULL)`) io.WriteString(c.w, `IS NULL)`)
} else { } else {
io.WriteString(c.w, `IS NOT NULL)`) io.WriteString(c.w, `IS NOT NULL)`)
} }
valExists = false return nil
case qcode.OpEqID: case qcode.OpEqID:
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)
@ -841,36 +956,28 @@ func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error
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)
io.WriteString(c.w, `") @@ to_tsquery('`) io.WriteString(c.w, `") @@ to_tsquery('`)
io.WriteString(c.w, val.Val) io.WriteString(c.w, ex.Val)
io.WriteString(c.w, `'))`) io.WriteString(c.w, `'))`)
valExists = false return nil
default: default:
return fmt.Errorf("[Where] unexpected op code %d", val.Op) return fmt.Errorf("[Where] unexpected op code %d", ex.Op)
} }
if valExists { if ex.Type == qcode.ValList {
if val.Type == qcode.ValList { c.renderList(ex)
c.renderList(val)
} else { } else {
c.renderVal(val, c.vars) c.renderVal(ex, c.vars, col)
} }
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
}
qcode.FreeExp(val)
}
default:
return fmt.Errorf("12: unexpected value %v (%t)", intf, intf)
}
}
return nil return nil
} }
@ -944,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 {
@ -961,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 {
@ -969,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, `)`)
} }

View File

@ -28,7 +28,7 @@ func withComplexArgs(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0" ORDER BY "products_0_price_ob" DESC), '[]') AS "products" FROM (SELECT DISTINCT ON ("products_0_price_ob") row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_json_0", "products_0"."price" AS "products_0_price_ob" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") < 28) AND (("products"."id") >= 20)) LIMIT ('30') :: integer) AS "products_0" ORDER BY "products_0_price_ob" DESC LIMIT ('30') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0" ORDER BY "products_0_price_ob" DESC), '[]') AS "json_0" FROM (SELECT DISTINCT ON ("products_0_price_ob") row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0", "products_0"."price" AS "products_0_price_ob" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") < 28) AND (("products"."id") >= 20)) LIMIT ('30') :: integer) AS "products_0" ORDER BY "products_0_price_ob" DESC LIMIT ('30') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -56,7 +56,7 @@ func withWhereMultiOr(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."price") < 20) OR (("products"."price") > 10) OR NOT (("products"."id") IS NULL)) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."price") < 20) OR (("products"."price") > 10) OR NOT (("products"."id") IS NULL)) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -82,7 +82,7 @@ func withWhereIsNull(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."price") > 10) AND NOT (("products"."id") IS NULL)) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."price") > 10) AND NOT (("products"."id") IS NULL)) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -108,7 +108,7 @@ func withWhereAndList(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."price") > 10) AND NOT (("products"."id") IS NULL)) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."price") > 10) AND NOT (("products"."id") IS NULL)) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -128,7 +128,7 @@ func fetchByID(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 15)) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "done_1337"` sql := `SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 15)) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -148,7 +148,7 @@ func searchQuery(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("tsv") @@ to_tsquery('Imperial'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("tsv") @@ to_tsquery('Imperial'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -171,7 +171,7 @@ func oneToMany(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."products" AS "products") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("sel_json_1"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "sel_1")) AS "sel_json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "sel_json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -194,7 +194,7 @@ func belongsTo(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "users_1_join"."users" AS "users") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8)) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("sel_json_1"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "users_1"."email" AS "email") AS "sel_1")) AS "sel_json_1" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1" LIMIT ('20') :: integer) AS "sel_json_agg_1") AS "users_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "users_1_join"."json_1" AS "users") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8)) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "users_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -217,7 +217,7 @@ func manyToMany(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "customers_1_join"."customers" AS "customers") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8)) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("sel_json_1"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "sel_1")) AS "sel_json_1" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1" LIMIT ('20') :: integer) AS "sel_json_agg_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "customers_1_join"."json_1" AS "customers") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8)) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "json_row_1")) AS "json_1" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -240,7 +240,7 @@ func manyToManyReverse(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('customers', customers) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."products" AS "products") AS "sel_0")) AS "sel_json_0" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("sel_json_1"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name") AS "sel_1")) AS "sel_json_1" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "sel_json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('customers', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -260,7 +260,7 @@ func aggFunction(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8)) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8)) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "user") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -280,7 +280,7 @@ func aggFunctionBlockedByCol(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "anon") resSQL, err := compileGQLToPSQL(gql, nil, "anon")
if err != nil { if err != nil {
@ -300,7 +300,7 @@ func aggFunctionDisabled(t *testing.T) {
} }
}` }`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "anon1") resSQL, err := compileGQLToPSQL(gql, nil, "anon1")
if err != nil { if err != nil {
@ -320,7 +320,26 @@ 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")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func syntheticTables(t *testing.T) {
gql := `query {
me {
email
}
}`
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 {
@ -340,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 {
@ -352,14 +371,21 @@ func queryWithVariables(t *testing.T) {
} }
} }
func syntheticTables(t *testing.T) { func withWhereOnRelations(t *testing.T) {
gql := `query { gql := `query {
me { users(where: {
not: {
products: {
price: { gt: 3 }
}
}
}) {
id
email email
} }
}` }`
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('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") resSQL, err := compileGQLToPSQL(gql, nil, "user")
if err != nil { if err != nil {
@ -371,6 +397,80 @@ func syntheticTables(t *testing.T) {
} }
} }
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")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func blockedQuery(t *testing.T) {
gql := `query {
user(id: 5, where: { id: { gt: 3 } }) {
id
full_name
email
}
}`
sql := `SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func blockedFunctions(t *testing.T) {
gql := `query {
users {
count_id
email
}
}`
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE (false) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func TestCompileQuery(t *testing.T) { func TestCompileQuery(t *testing.T) {
t.Run("withComplexArgs", withComplexArgs) t.Run("withComplexArgs", withComplexArgs)
t.Run("withWhereAndList", withWhereAndList) t.Run("withWhereAndList", withWhereAndList)
@ -388,6 +488,8 @@ func TestCompileQuery(t *testing.T) {
t.Run("aggFunctionWithFilter", aggFunctionWithFilter) t.Run("aggFunctionWithFilter", aggFunctionWithFilter)
t.Run("syntheticTables", syntheticTables) t.Run("syntheticTables", syntheticTables)
t.Run("queryWithVariables", queryWithVariables) t.Run("queryWithVariables", queryWithVariables)
t.Run("withWhereOnRelations", withWhereOnRelations)
t.Run("multiRoot", multiRoot)
t.Run("blockedQuery", blockedQuery) t.Run("blockedQuery", blockedQuery)
t.Run("blockedFunctions", blockedFunctions) t.Run("blockedFunctions", blockedFunctions)
} }
@ -415,47 +517,6 @@ var benchGQL = []byte(`query {
} }
}`) }`)
func blockedQuery(t *testing.T) {
gql := `query {
user(id: 5, where: { id: { gt: 3 } }) {
id
full_name
email
}
}`
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"`
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func blockedFunctions(t *testing.T) {
gql := `query {
users {
count_id
email
}
}`
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"`
resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func BenchmarkCompile(b *testing.B) { func BenchmarkCompile(b *testing.B) {
w := &bytes.Buffer{} w := &bytes.Buffer{}

2
qcode/cleanup.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
cd corpus && rm -rf $(find . ! -name '00?.gql')

View File

@ -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
}

View File

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

View File

@ -148,25 +148,14 @@ 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()
pid, ok := intf.(int32) if pid, ok := intf.(int32); ok {
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) {

View File

@ -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)

View File

@ -20,6 +20,7 @@ const (
const ( const (
QTQuery QType = iota + 1 QTQuery QType = iota + 1
QTMutation
QTInsert QTInsert
QTUpdate QTUpdate
QTDelete QTDelete
@ -30,6 +31,8 @@ type QCode struct {
Type QType Type QType
ActionVar string ActionVar string
Selects []Select Selects []Select
Roots []int32
rootsA [5]int32
} }
type Select struct { type Select struct {
@ -59,7 +62,7 @@ type Column struct {
type Exp struct { type Exp struct {
Op ExpOp Op ExpOp
Col string Col string
NestedCol bool NestedCols []string
Type ValType Type ValType
Val string Val string
ListType ValType ListType ValType
@ -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,18 +375,26 @@ 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 {
return
}
switch fil.Op { switch fil.Op {
case OpNop: case OpNop:
case OpFalse: case OpFalse:
root.Where = fil root.Where = fil
default: default:
if root.Where != nil { if root.Where != nil {
ow := root.Where ow := root.Where
@ -388,10 +409,6 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
root.Where = fil root.Where = fil
} }
} }
}
qc.Selects = selects[:id]
return nil
} }
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
@ -918,10 +935,8 @@ func setWhereColName(ex *Exp, node *Node) {
} }
if len(list) == 1 { if len(list) == 1 {
ex.Col = list[0] ex.Col = list[0]
} else if len(list) > 1 {
} else if len(list) > 2 { ex.NestedCols = list
ex.Col = buildPath(list)
ex.NestedCol = true
} }
} }

23
qcode/utils.go Normal file
View File

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

View File

@ -18,6 +18,8 @@ const (
) )
type allowItem struct { type allowItem struct {
name string
hash string
uri string uri string
gql string gql string
vars json.RawMessage vars json.RawMessage
@ -46,7 +48,7 @@ func initAllowList(cpath string) {
if _, err := os.Stat(fp); err == nil { if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp _allowList.filepath = fp
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
@ -56,7 +58,7 @@ func initAllowList(cpath string) {
if _, err := os.Stat(fp); err == nil { if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp _allowList.filepath = fp
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
@ -66,13 +68,13 @@ func initAllowList(cpath string) {
if _, err := os.Stat(fp); err == nil { if _, err := os.Stat(fp); err == nil {
_allowList.filepath = fp _allowList.filepath = fp
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
if len(_allowList.filepath) == 0 { if len(_allowList.filepath) == 0 {
if conf.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 {
@ -94,7 +96,7 @@ func initAllowList(cpath string) {
} }
func (al *allowList) add(req *gqlReq) { func (al *allowList) add(req *gqlReq) {
if al.active == false || len(req.ref) == 0 || len(req.Query) == 0 { if len(req.ref) == 0 || len(req.Query) == 0 {
return return
} }
@ -119,11 +121,39 @@ func (al *allowList) add(req *gqlReq) {
} }
} }
func (al *allowList) load() { func (al *allowList) upsert(query, vars []byte, uri string) {
if al.active == false { q := string(query)
return hash := gqlHash(q, vars, "")
name := gqlName(q)
var key string
if len(name) == 0 {
key = hash
} else {
key = name
} }
if i, ok := al.index[key]; !ok {
al.list = append(al.list, &allowItem{
name: name,
hash: hash,
uri: uri,
gql: q,
vars: vars,
})
al.index[key] = len(al.list) - 1
} else {
item := al.list[i]
item.name = name
item.hash = hash
item.gql = q
item.vars = vars
}
}
func (al *allowList) load() {
b, err := ioutil.ReadFile(al.filepath) b, err := ioutil.ReadFile(al.filepath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -172,22 +202,7 @@ func (al *allowList) load() {
if c == 0 { if c == 0 {
if ty == AL_QUERY { if ty == AL_QUERY {
q := string(b[s:(e + 1)]) al.upsert(b[s:(e+1)], varBytes, uri)
key := gqlHash(q, varBytes, "")
if idx, ok := al.index[key]; !ok {
al.list = append(al.list, &allowItem{
uri: uri,
gql: q,
vars: varBytes,
})
al.index[key] = len(al.list) - 1
} else {
item := al.list[idx]
item.gql = q
item.vars = varBytes
}
varBytes = nil varBytes = nil
} else if ty == AL_VARS { } else if ty == AL_VARS {
@ -205,18 +220,32 @@ func (al *allowList) load() {
} }
func (al *allowList) save(item *allowItem) { func (al *allowList) save(item *allowItem) {
if al.active == false { item.hash = gqlHash(item.gql, item.vars, "")
item.name = gqlName(item.gql)
if len(item.name) == 0 {
key := item.hash
if _, ok := al.index[key]; ok {
return return
} }
key := gqlHash(item.gql, item.vars, "") al.list = append(al.list, item)
al.index[key] = len(al.list) - 1
if idx, ok := al.index[key]; ok { } else {
al.list[idx] = item key := item.name
if i, ok := al.index[key]; ok {
if al.list[i].hash == item.hash {
return
}
al.list[i] = item
} else { } else {
al.list = append(al.list, item) al.list = append(al.list, item)
al.index[key] = len(al.list) - 1 al.index[key] = len(al.list) - 1
} }
}
f, err := os.Create(al.filepath) f, err := os.Create(al.filepath)
if err != nil { if err != nil {

94
serv/args.go Normal file
View File

@ -0,0 +1,94 @@
package serv
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"github.com/dosco/super-graph/jsn"
)
func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int, error) {
return func(w io.Writer, tag string) (int, error) {
switch tag {
case "user_id_provider":
if v := ctx.Value(userIDProviderKey); v != nil {
return io.WriteString(w, v.(string))
}
return 0, errors.New("query requires variable $user_id_provider")
case "user_id":
if v := ctx.Value(userIDKey); v != nil {
return io.WriteString(w, v.(string))
}
return 0, errors.New("query requires variable $user_id")
case "user_role":
if v := ctx.Value(userRoleKey); v != nil {
return io.WriteString(w, v.(string))
}
return 0, errors.New("query requires variable $user_role")
}
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
if len(fields) == 0 {
return 0, nil
}
return w.Write(fields[0].Value)
}
}
func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
vars := make([]interface{}, len(args))
var fields map[string]interface{}
var err error
if len(ctx.req.Vars) != 0 {
fields, _, err = jsn.Tree(ctx.req.Vars)
if err != nil {
return nil, err
}
}
for i := range args {
av := args[i]
switch {
case bytes.Equal(av, []byte("user_id")):
if v := ctx.Value(userIDKey); v != nil {
vars[i] = v.(string)
} else {
return nil, errors.New("query requires variable $user_id")
}
case bytes.Equal(av, []byte("user_id_provider")):
if v := ctx.Value(userIDProviderKey); v != nil {
vars[i] = v.(string)
} else {
return nil, errors.New("query requires variable $user_id_provider")
}
case bytes.Equal(av, []byte("user_role")):
if v := ctx.Value(userRoleKey); v != nil {
vars[i] = v.(string)
} else {
return nil, errors.New("query requires variable $user_role")
}
default:
if v, ok := fields[string(av)]; ok {
vars[i] = v
} else {
return nil, fmt.Errorf("query requires variable $%s", string(av))
}
}
}
return vars, nil
}

View File

@ -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)
} }
} }

View File

@ -15,11 +15,11 @@ import (
func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc { func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
cookie := conf.Auth.Cookie cookie := conf.Auth.Cookie
if len(cookie) == 0 { if len(cookie) == 0 {
logger.Fatal().Msg("no auth.cookie defined") errlog.Fatal().Msg("no auth.cookie defined")
} }
if len(conf.Auth.Rails.URL) == 0 { if len(conf.Auth.Rails.URL) == 0 {
logger.Fatal().Msg("no auth.rails.url defined") errlog.Fatal().Msg("no auth.rails.url defined")
} }
rp := &redis.Pool{ rp := &redis.Pool{
@ -28,13 +28,13 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
Dial: func() (redis.Conn, error) { Dial: func() (redis.Conn, error) {
c, err := redis.DialURL(conf.Auth.Rails.URL) c, err := redis.DialURL(conf.Auth.Rails.URL)
if err != nil { if err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
pwd := conf.Auth.Rails.Password pwd := conf.Auth.Rails.Password
if len(pwd) != 0 { if len(pwd) != 0 {
if _, err := c.Do("AUTH", pwd); err != nil { if _, err := c.Do("AUTH", pwd); err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
} }
return c, err return c, err
@ -69,16 +69,16 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc { func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc {
cookie := conf.Auth.Cookie cookie := conf.Auth.Cookie
if len(cookie) == 0 { if len(cookie) == 0 {
logger.Fatal().Msg("no auth.cookie defined") errlog.Fatal().Msg("no auth.cookie defined")
} }
if len(conf.Auth.Rails.URL) == 0 { if len(conf.Auth.Rails.URL) == 0 {
logger.Fatal().Msg("no auth.rails.url defined") errlog.Fatal().Msg("no auth.rails.url defined")
} }
rURL, err := url.Parse(conf.Auth.Rails.URL) rURL, err := url.Parse(conf.Auth.Rails.URL)
if err != nil { if err != nil {
logger.Fatal().Err(err).Send() errlog.Fatal().Err(err).Send()
} }
mc := memcache.New(rURL.Host) mc := memcache.New(rURL.Host)
@ -111,17 +111,17 @@ 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) {
ck, err := r.Cookie(cookie) ck, err := r.Cookie(cookie)
if err != nil { if err != nil || len(ck.Value) == 0 {
logger.Warn().Err(err).Msg("rails cookie missing") logger.Warn().Err(err).Msg("rails cookie missing")
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return

View File

@ -19,24 +19,21 @@ import (
const ( const (
serverName = "Super Graph" serverName = "Super Graph"
authFailBlockAlways = iota + 1
authFailBlockPerQuery
authFailBlockNever
) )
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
authFailBlock int
) )
func Init() { func Init() {
logger = initLog() initLog()
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: "super-graph", Use: "super-graph",
@ -115,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",
@ -133,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) {
@ -164,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"))
} }
@ -179,11 +178,9 @@ func initConf() (*config, error) {
return nil, fmt.Errorf("unable to decode config, %v", err) return nil, fmt.Errorf("unable to decode config, %v", err)
} }
authFailBlock = getAuthFailBlock(c)
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)
@ -218,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 {
@ -253,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
@ -276,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")
}
} }
} }

View File

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

View File

@ -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

View File

@ -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)

View File

@ -1,6 +1,7 @@
package serv package serv
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -12,20 +13,21 @@ import (
"github.com/brianvoe/gofakeit" "github.com/brianvoe/gofakeit"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/valyala/fasttemplate"
) )
func cmdDBSeed(cmd *cobra.Command, args []string) { func cmdDBSeed(cmd *cobra.Command, args []string) {
var err error var err error
if conf, err = initConf(); err != nil { if conf, err = initConf(); err != nil {
logger.Fatal().Err(err).Msg("failed to read config") errlog.Fatal().Err(err).Msg("failed to read config")
} }
conf.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)

View File

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

View File

@ -23,8 +23,9 @@ 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 string `mapstructure:"auth_fail_block"` AuthFailBlock bool `mapstructure:"auth_fail_block"`
SeedFile string `mapstructure:"seed_file"` SeedFile string `mapstructure:"seed_file"`
MigrationsPath string `mapstructure:"migrations_path"` MigrationsPath string `mapstructure:"migrations_path"`
@ -65,14 +66,10 @@ type config struct {
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"`
Vars map[string]string `mapstructure:"variables"` Vars map[string]string `mapstructure:"variables"`
Defaults struct {
Filters []string
Blocklist []string Blocklist []string
}
Tables []configTable Tables []configTable
} `mapstructure:"database"` } `mapstructure:"database"`
@ -81,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 {
@ -103,42 +101,48 @@ type configRemote struct {
} `mapstructure:"set_headers"` } `mapstructure:"set_headers"`
} }
type configRoleTable struct { type configQuery struct {
Name string
Query struct {
Limit int Limit int
Filters []string Filters []string
Columns []string Columns []string
DisableFunctions bool `mapstructure:"disable_functions"` DisableFunctions bool `mapstructure:"disable_functions"`
Block bool Block bool
} }
Insert struct { type configInsert struct {
Filters []string Filters []string
Columns []string Columns []string
Presets map[string]string Presets map[string]string
Block bool Block bool
} }
Update struct { type configUpdate struct {
Filters []string Filters []string
Columns []string Columns []string
Presets map[string]string Presets map[string]string
Block bool Block bool
} }
Delete struct { type configDelete struct {
Filters []string Filters []string
Columns []string Columns []string
Block bool Block bool
} }
type configRoleTable struct {
Name string
Query configQuery
Insert configInsert
Update configUpdate
Delete configDelete
} }
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 {
@ -189,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)
} }
@ -209,26 +217,34 @@ 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]
} }
if _, ok := rolesMap["user"]; !ok { c.roles[role.Name] = role
c.Roles = append(c.Roles, configRole{Name: "user"})
} }
if _, ok := rolesMap["anon"]; !ok { if _, ok := c.roles["user"]; !ok {
c.Roles = append(c.Roles, configRole{Name: "anon"}) u := configRole{Name: "user"}
c.Roles = append(c.Roles, u)
c.roles["user"] = &u
}
if _, ok := c.roles["anon"]; !ok {
logger.Warn().Msg("unauthenticated requests will be blocked. no role 'anon' defined")
c.AuthFailBlock = true
} }
c.validate() c.validate()
@ -243,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{}{}
} }
@ -254,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{}{}
} }

View File

@ -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,92 +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
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 return nil, nil, err
} }
defer tx.Rollback(c) defer tx.Rollback(c)
}
if v := c.Value(userIDKey); v != nil { if conf.DB.SetUserID {
_, err = tx.Exec(c, fmt.Sprintf(`SET LOCAL "user.id" = %s;`, v)) if err := setLocalUserID(c, tx); err != nil {
if 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 {
@ -142,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
} }
@ -153,71 +118,92 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) {
} }
var root []byte var root []byte
vars := varList(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 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
}
c.req.role = role
if useTx {
if err := tx.Commit(c); err != nil { if err := tx.Commit(c); err != nil {
return nil, nil, err return nil, nil, err
} }
}
return root, ps, nil 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)
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)
}
mutation := isMutation(c.req.Query) if conf.DB.SetUserID {
useRoleQuery := len(conf.RolesQuery) != 0 && mutation 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, varMap(c))
if err == errNoUserID &&
authFailBlock == authFailBlockPerQuery &&
authCheck(c) == false {
return nil, 0, errUnauthorized
}
_, 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
@ -226,194 +212,57 @@ func (c *coreContext) resolveSQL() ([]byte, uint32, error) {
stime = time.Now() stime = time.Now()
} }
if v := c.Value(userIDKey); v != nil {
_, err = tx.Exec(c, fmt.Sprintf(`SET LOCAL "user.id" = %s;`, v))
if err != nil {
return nil, 0, err
}
}
var root []byte var root []byte
var role string
var row pgx.Row
if mutation { defaultRole := c.req.role
err = tx.QueryRow(c, finalSQL).Scan(&root)
if useTx {
row = tx.QueryRow(c, finalSQL)
} else { } else {
err = tx.QueryRow(c, finalSQL).Scan(&c.req.role, &root) 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 { if err != nil {
return nil, 0, err return nil, nil, err
} }
if useTx {
if err := tx.Commit(c); err != nil { if err := tx.Commit(c); err != nil {
return nil, 0, err return nil, nil, err
}
} }
if mutation { if conf.Production == false {
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) _allowList.add(&c.req)
} }
return root, st.skipped, nil if len(stmts) > 1 {
} if st = findStmt(role, stmts); st == nil {
return nil, nil, fmt.Errorf("invalid role '%s' returned", role)
func (c *coreContext) resolveRemote(
hdr http.Header,
h *xxhash.Digest,
field jsn.Field,
sel []qcode.Select,
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
// replacement data for the marked insertion points
// key and value will be replaced by whats below
toA := [1]jsn.Field{}
to := toA[:1]
// use the json key to find the related Select object
k1 := xxhash.Sum64(field.Key)
s, ok := sfmap[k1]
if !ok {
return nil, nil
} }
p := sel[s.ParentID]
// then use the Table nme in the Select and it's parent
// to find the resolver to use for this relationship
k2 := mkkey(h, s.Table, p.Table)
r, ok := rmap[k2]
if !ok {
return nil, nil
}
id := jsn.Value(field.Value)
if len(id) == 0 {
return nil, nil
}
st := time.Now()
b, err := r.Fn(hdr, id)
if err != nil {
return nil, err
} }
if conf.EnableTracing { if conf.EnableTracing {
c.addTrace(sel, s.ID, st) for _, id := range st.qc.Roots {
c.addTrace(st.qc.Selects, id, stime)
}
} }
if len(r.Path) != 0 { return root, st, nil
b = jsn.Strip(b, r.Path)
}
var ob bytes.Buffer
if len(s.Cols) != 0 {
err = jsn.Filter(&ob, b, colsToList(s.Cols))
if err != nil {
return nil, err
}
} else {
ob.WriteString("null")
}
to[0] = jsn.Field{[]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) {
@ -448,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)
@ -456,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--
@ -518,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)
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"github.com/dosco/super-graph/psql" "github.com/dosco/super-graph/psql"
@ -17,48 +18,95 @@ 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
} }
// For the 'anon' role in production only compile
// 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{}
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)) stmts := make([]stmt, 0, len(conf.Roles))
mutation := (qc.Type != qcode.QTQuery)
w := &bytes.Buffer{} w := &bytes.Buffer{}
for i := range conf.Roles { 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 i > 0 {
qc, err = qcompile.Compile(gql, role.Name)
if err != nil { if err != nil {
return nil, err 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
} }
@ -68,42 +116,41 @@ func (c *coreContext) buildStmt() ([]stmt, error) {
s.sql = w.String() s.sql = w.String()
w.Reset() w.Reset()
} }
sql, err := renderUserQuery(stmts, vm)
if err != nil {
return nil, err
} }
if mutation { stmts[0].sql = sql
return stmts, nil 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, `VALUES ("`)
if v != nil {
io.WriteString(w, v.(string))
} else {
io.WriteString(w, c.req.role)
}
io.WriteString(w, `")) AS "_sg_auth_info"(role) LIMIT 1;`)
} else {
io.WriteString(w, `SELECT (CASE WHEN EXISTS (`)
io.WriteString(w, conf.RolesQuery) io.WriteString(w, conf.RolesQuery)
io.WriteString(w, `) THEN `) io.WriteString(w, `) THEN `)
@ -119,26 +166,23 @@ func (c *coreContext) buildStmt() ([]stmt, error) {
io.WriteString(w, `'`) io.WriteString(w, `'`)
} }
if len(c.req.role) == 0 { io.WriteString(w, ` ELSE 'user' END) FROM (`)
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, conf.RolesQuery)
io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) ELSE '`) io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) `)
if len(c.req.role) == 0 { io.WriteString(w, `ELSE 'anon' END) FROM (VALUES (1)) AS "_sg_auth_filler") AS "_sg_auth_info"(role) LIMIT 1; `)
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() return w.String(), nil
stmts[0].role = nil }
return stmts, nil func hasTablesWithConfig(qc *qcode.QCode, role *configRole) bool {
for _, id := range qc.Roots {
t, err := schema.GetTable(qc.Selects[id].Table)
if err != nil {
return false
}
if _, ok := role.tablesMap[t.Name]; !ok {
return false
}
}
return true
} }

197
serv/core_remote.go Normal file
View File

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

View File

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

View File

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

View File

@ -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")
) )
@ -70,26 +69,23 @@ type resolver struct {
func apiv1Http(w http.ResponseWriter, r *http.Request) { func apiv1Http(w http.ResponseWriter, r *http.Request) {
ctx := &coreContext{Context: r.Context()} ctx := &coreContext{Context: r.Context()}
if authFailBlock == authFailBlockAlways && authCheck(ctx) == false { if conf.AuthFailBlock && authCheck(ctx) == false {
err := "Not authorized" w.WriteHeader(http.StatusUnauthorized)
logger.Debug().Msg(err) json.NewEncoder(w).Encode(gqlResp{Error: errUnauthorized.Error()})
http.Error(w, err, 401)
return return
} }
b, err := ioutil.ReadAll(io.LimitReader(r.Body, maxReadBytes)) b, err := ioutil.ReadAll(io.LimitReader(r.Body, maxReadBytes))
defer r.Body.Close()
if err != nil { if err != nil {
logger.Err(err).Msg("failed to read request body") errlog.Error().Err(err).Msg("failed to read request body")
errorResp(w, err) errorResp(w, err)
return return
} }
defer r.Body.Close()
err = json.Unmarshal(b, &ctx.req) err = json.Unmarshal(b, &ctx.req)
if err != nil { if err != nil {
logger.Err(err).Msg("failed to decode json request body") errlog.Error().Err(err).Msg("failed to decode json request body")
errorResp(w, err) errorResp(w, err)
return return
} }
@ -108,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()})
} }

View File

@ -3,20 +3,19 @@ package serv
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
"github.com/jackc/pgconn" "github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
) )
type preparedItem struct { type preparedItem struct {
stmt *pgconn.StatementDescription sd *pgconn.StatementDescription
args [][]byte args [][]byte
skipped uint32 st *stmt
qc *qcode.QCode
} }
var ( var (
@ -24,83 +23,125 @@ 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 return err
} }
defer tx.Rollback(c)
switch qt {
case qcode.QTQuery:
stmts1, err := buildMultiStmt(q, vars)
if err != nil {
return err
}
err = prepare(c, tx, &stmts1[0], gqlHash(gql, vars, "user"))
if err != nil {
return err
}
stmts2, err := buildRoleStmt(q, vars, "anon")
if err != nil {
return err
}
err = prepare(c, tx, &stmts2[0], gqlHash(gql, vars, "anon"))
if err != nil {
return err
}
case qcode.QTMutation:
for _, role := range conf.Roles {
stmts, err := buildRoleStmt(q, vars, role.Name)
if err != nil {
return err
}
err = prepare(c, tx, &stmts[0], gqlHash(gql, vars, role.Name))
if err != nil {
return err
}
}
}
if len(vars) == 0 {
logger.Debug().Msgf("Building prepared statement for:\n %s", gql)
} else {
logger.Debug().Msgf("Building prepared statement:\n %s\n%s", vars, gql)
}
if err := tx.Commit(c); err != nil {
return err
} }
return nil return nil
} }
func prepareRoleStmt() error { func prepare(c context.Context, tx pgx.Tx, st *stmt, key string) error {
finalSQL, am := processTemplate(st.sql)
sd, err := tx.Prepare(c, "", finalSQL)
if err != nil {
return err
}
_preparedList[key] = &preparedItem{
sd: sd,
args: am,
st: st,
}
return nil
}
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 +166,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 +175,31 @@ func prepareRoleStmt() error {
} }
func processTemplate(tmpl string) (string, [][]byte) { func processTemplate(tmpl string) (string, [][]byte) {
t := fasttemplate.New(tmpl, `{{`, `}}`) st := struct {
am := make([][]byte, 0, 5) vmap map[string]int
i := 0 am [][]byte
i int
}{
vmap: make(map[string]int),
am: make([][]byte, 0, 5),
i: 0,
}
vmap := make(map[string]int) execFunc := func(w io.Writer, tag string) (int, error) {
if n, ok := st.vmap[tag]; ok {
return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
if n, ok := vmap[tag]; ok {
return w.Write([]byte(fmt.Sprintf("$%d", n))) return w.Write([]byte(fmt.Sprintf("$%d", n)))
} }
am = append(am, []byte(tag)) st.am = append(st.am, []byte(tag))
i++ st.i++
vmap[tag] = i st.vmap[tag] = st.i
return w.Write([]byte(fmt.Sprintf("$%d", i))) return w.Write([]byte(fmt.Sprintf("$%d", st.i)))
}), am }
t1 := fasttemplate.New(tmpl, `'{{`, `}}'`)
ts1 := t1.ExecuteFuncString(execFunc)
t2 := fasttemplate.New(ts1, `{{`, `}}`)
ts2 := t2.ExecuteFuncString(execFunc)
return ts2, st.am
} }

View File

@ -107,6 +107,13 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
case event := <-watcher.Events: case event := <-watcher.Events:
// Ensure that we use the correct events, as they are not uniform across // Ensure that we use the correct events, as they are not uniform across
// platforms. See https://github.com/fsnotify/fsnotify/issues/74 // platforms. See https://github.com/fsnotify/fsnotify/issues/74
if conf.Production == false && strings.HasSuffix(event.Name, "/allow.list") {
continue
}
logger.Info().Msgf("Reloading, file changed detected '%s'", event)
var trigger bool var trigger bool
switch runtime.GOOS { switch runtime.GOOS {
case "darwin", "freebsd", "openbsd", "netbsd", "dragonfly": case "darwin", "freebsd", "openbsd", "netbsd", "dragonfly":
@ -161,7 +168,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
func ReExec() { func ReExec() {
err := syscall.Exec(binSelf, append([]string{binSelf}, os.Args[1:]...), os.Environ()) err := syscall.Exec(binSelf, append([]string{binSelf}, os.Args[1:]...), os.Environ())
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("cannot restart") errlog.Fatal().Err(err).Msg("cannot restart")
} }
} }

View File

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

View File

@ -15,13 +15,15 @@ import (
) )
func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) { func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
schema, err := psql.NewDBSchema(db, c.getAliasMap()) var err error
schema, err = psql.NewDBSchema(db, c.getAliasMap())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
conf := qcode.Config{ conf := qcode.Config{
Blocklist: c.DB.Defaults.Blocklist, Blocklist: c.DB.Blocklist,
KeepArgs: false, KeepArgs: false,
} }
@ -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()))
} }
@ -204,16 +203,3 @@ func getConfigName() string {
return ge return ge
} }
func getAuthFailBlock(c *config) int {
switch c.AuthFailBlock {
case "always":
return authFailBlockAlways
case "per_query", "perquery", "query":
return authFailBlockPerQuery
case "never", "false":
return authFailBlockNever
}
return authFailBlockAlways
}

View File

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

View File

@ -106,17 +106,28 @@ func al(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
} }
func isMutation(sql string) bool { func gqlName(b string) string {
for i := range sql { state, s := 0, 0
b := sql[i]
if b == '{' { for i := 0; i < len(b); i++ {
return false switch {
} case state == 2 && b[i] == '{':
if al(b) { return b[s:i]
return (b == 'm' || b == 'M') case state == 2 && b[i] == ' ':
return b[s:i]
case state == 1 && b[i] == '{':
return ""
case state == 1 && b[i] != ' ':
s = i
state = 2
case state == 1 && b[i] == ' ':
continue
case i != 0 && b[i] == ' ' && (b[i-1] == 'n' || b[i-1] == 'y'):
state = 1
} }
} }
return false
return ""
} }
func findStmt(role string, stmts []stmt) *stmt { func findStmt(role string, stmts []stmt) *stmt {

View File

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

View File

@ -1,117 +0,0 @@
package serv
import (
"bytes"
"fmt"
"io"
"github.com/dosco/super-graph/jsn"
)
func varMap(ctx *coreContext) func(w io.Writer, tag string) (int, error) {
return func(w io.Writer, tag string) (int, error) {
switch tag {
case "user_id_provider":
if v := ctx.Value(userIDProviderKey); v != nil {
return stringVar(w, v.(string))
}
io.WriteString(w, "null")
return 0, nil
case "user_id":
if v := ctx.Value(userIDKey); v != nil {
return stringVar(w, v.(string))
}
io.WriteString(w, "null")
return 0, nil
case "user_role":
if v := ctx.Value(userRoleKey); v != nil {
return stringVar(w, v.(string))
}
io.WriteString(w, "null")
return 0, nil
}
fields := jsn.Get(ctx.req.Vars, [][]byte{[]byte(tag)})
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 stringVarB(w, fields[0].Value)
}
w.Write(fields[0].Value)
return 0, nil
}
}
func varList(ctx *coreContext, args [][]byte) []interface{} {
vars := make([]interface{}, len(args))
var fields map[string]interface{}
var err error
if len(ctx.req.Vars) != 0 {
fields, _, err = jsn.Tree(ctx.req.Vars)
if err != nil {
logger.Warn().Err(err).Msg("Failed to parse variables")
}
}
for i := range args {
av := args[i]
switch {
case bytes.Equal(av, []byte("user_id")):
if v := ctx.Value(userIDKey); v != nil {
vars[i] = v.(string)
}
case bytes.Equal(av, []byte("user_id_provider")):
if v := ctx.Value(userIDProviderKey); v != nil {
vars[i] = v.(string)
}
default:
if v, ok := fields[string(av)]; ok {
vars[i] = v
}
}
}
return vars
}
func stringVar(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 stringVarB(w io.Writer, v []byte) (int, error) {
if n, err := w.Write([]byte(`'`)); err != nil {
return n, err
}
if n, err := w.Write(v); err != nil {
return n, err
}
return w.Write([]byte(`'`))
}

View File

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

View File

@ -1,19 +1,18 @@
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
# valid values: always, per_query, never auth_fail_block: false
auth_fail_block: never
# Latency tracing for database queries and remote joins # Latency tracing for database queries and remote joins
# the resulting latency information is returned with the # the resulting latency information is returned with the
@ -49,10 +48,11 @@ 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. Good for testing # the user_id via a header for testing.
# Disable in production
creds_in_header: true creds_in_header: true
rails: rails:
@ -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: ''
@ -93,14 +93,13 @@ database:
#max_retries: 0 #max_retries: 0
#log_level: "debug" #log_level: "debug"
# Define variables here that you want to use in filters # Set session variable "user.id" to the user id
# sub-queries must be wrapped in () # Enable this if you need the user id in triggers, etc
variables: set_user_id: false
account_id: "(select account_id from users where id = $user_id)"
# Define defaults to for the field key and values below # Define additional variables here to be used with filters
defaults: variables:
# filters: ["{ user_id: { eq: $user_id } }"] admin_account_id: "5"
# Field and table names that you wish to block # Field and table names that you wish to block
blocklist: blocklist:
@ -132,7 +131,7 @@ tables:
name: me name: me
table: users table: users
roles_query: "SELECT * FROM users as usr WHERE id = $user_id" roles_query: "SELECT * FROM users WHERE id = $user_id"
roles: roles:
- name: anon - name: anon
@ -181,11 +180,10 @@ roles:
- updated_at: "now" - updated_at: "now"
delete: delete:
deny: true block: true
- name: admin - name: admin
match: id = 1 match: id = 1000
tables: tables:
- name: users - name: users
# query: filters: []
# filters: ["{ account_id: { _eq: $account_id } }"]

View File

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

View File

@ -2,22 +2,20 @@
# 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
# valid values: always, per_query, never auth_fail_block: true
auth_fail_block: always
# Latency tracing for database queries and remote joins # Latency tracing for database queries and remote joins
# the resulting latency information is returned with the # the resulting latency information is returned with the
@ -42,47 +40,17 @@ 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
#max_retries: 0 #max_retries: 0
#log_level: "debug" #log_level: "debug"
# Set session variable "user.id" to the user id
# Enable this if you need the user id in triggers, etc
set_user_id: false