Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
9140e597e1 | |||
94593c9cce | |||
a96c211fe5 | |||
6d47f0df8e | |||
e82bdbed65 | |||
ef51b99fc7 | |||
9ebd03fa8c | |||
aff2a13ba4 | |||
f518d5fc69 | |||
7583326d21 | |||
8024a8420b |
49
.chglog/CHANGELOG.tpl.md
Executable file
49
.chglog/CHANGELOG.tpl.md
Executable file
@ -0,0 +1,49 @@
|
||||
{{ if .Versions -}}
|
||||
<a name="unreleased"></a>
|
||||
## [Unreleased]
|
||||
|
||||
{{ if .Unreleased.CommitGroups -}}
|
||||
{{ range .Unreleased.CommitGroups -}}
|
||||
### {{ .Title }}
|
||||
{{ range .Commits -}}
|
||||
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
|
||||
{{ range .Versions }}
|
||||
<a name="{{ .Tag.Name }}"></a>
|
||||
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
|
||||
{{ range .CommitGroups -}}
|
||||
### {{ .Title }}
|
||||
{{ range .Commits -}}
|
||||
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
|
||||
{{- if .MergeCommits -}}
|
||||
### Pull Requests
|
||||
{{ range .MergeCommits -}}
|
||||
- {{ .Header }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
|
||||
{{- if .NoteGroups -}}
|
||||
{{ range .NoteGroups -}}
|
||||
### {{ .Title }}
|
||||
{{ range .Notes }}
|
||||
{{ .Body }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
|
||||
{{- if .Versions }}
|
||||
[Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD
|
||||
{{ range .Versions -}}
|
||||
{{ if .Tag.Previous -}}
|
||||
[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
{{ end -}}
|
27
.chglog/config.yml
Executable file
27
.chglog/config.yml
Executable file
@ -0,0 +1,27 @@
|
||||
style: github
|
||||
template: CHANGELOG.tpl.md
|
||||
info:
|
||||
title: CHANGELOG
|
||||
repository_url: https://github.com/dosco/super-graph
|
||||
options:
|
||||
commits:
|
||||
# filters:
|
||||
# Type:
|
||||
# - feat
|
||||
# - fix
|
||||
# - perf
|
||||
# - refactor
|
||||
commit_groups:
|
||||
# title_maps:
|
||||
# feat: Features
|
||||
# fix: Bug Fixes
|
||||
# perf: Performance Improvements
|
||||
# refactor: Code Refactoring
|
||||
header:
|
||||
pattern: "^((\\w+)\\s.*)$"
|
||||
pattern_maps:
|
||||
- Subject
|
||||
- Type
|
||||
notes:
|
||||
keywords:
|
||||
- BREAKING CHANGE
|
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: dosco
|
||||
|
||||
---
|
||||
|
||||
<!-- If you suspect this could be a bug, follow the template. -->
|
||||
|
||||
### What version of Super Graph are you using? `super-graph version`
|
||||
|
||||
|
||||
### Have you tried reproducing the issue with the latest release?
|
||||
|
||||
|
||||
### What is the hardware spec (RAM, OS)?
|
||||
|
||||
|
||||
### Steps to reproduce the issue (config used to run Super Graph).
|
||||
|
||||
|
||||
### Expected behaviour and actual result.
|
12
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Documentation
|
||||
about: Suggest how we can improve documentation
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: dosco
|
||||
|
||||
---
|
||||
|
||||
<!-- If you think the Super Graph documentation falls short https://supergraph.dev/guide.html please suggest ways we can improve it. -->
|
||||
|
||||
<!-- explain it here. -->
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: dosco
|
||||
|
||||
---
|
||||
|
||||
## Experience Report
|
||||
|
||||
Note: Feature requests are judged based on user experience this is similar to the [Go Experience Reports](https://github.com/golang/go/wiki/ExperienceReports). These reports should focus on the problems: they should not focus on and need not propose solutions.
|
||||
|
||||
### What you wanted to do
|
||||
|
||||
### What you actually did
|
||||
|
||||
### Why that wasn't great, with examples
|
||||
|
||||
### Any external references to support your case
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -27,8 +27,11 @@
|
||||
main
|
||||
.DS_Store
|
||||
.swp
|
||||
.release
|
||||
main
|
||||
super-graph
|
||||
supergraph
|
||||
*-fuzz.zip
|
||||
crashers
|
||||
suppressions
|
||||
suppressions
|
||||
release
|
384
CHANGELOG.md
Normal file
384
CHANGELOG.md
Normal file
@ -0,0 +1,384 @@
|
||||
<a name="unreleased"></a>
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
<a name="v0.12.4"></a>
|
||||
## [v0.12.4] - 2019-11-28
|
||||
### Move
|
||||
- Move license from MIT to Apache 2.0. Add Makefile
|
||||
|
||||
|
||||
<a name="v0.12.3"></a>
|
||||
## [v0.12.3] - 2019-11-26
|
||||
### Added
|
||||
- Added support for query names to the allow.list
|
||||
|
||||
|
||||
<a name="v0.12.2"></a>
|
||||
## [v0.12.2] - 2019-11-25
|
||||
### Fix
|
||||
- Fix bug with compiling anon queries
|
||||
|
||||
|
||||
<a name="v0.12.1"></a>
|
||||
## [v0.12.1] - 2019-11-22
|
||||
### Move
|
||||
- Move sql query logging from info to debug
|
||||
|
||||
|
||||
<a name="v0.12.0"></a>
|
||||
## [v0.12.0] - 2019-11-22
|
||||
### Use
|
||||
- Use logger error instead of panic in goja handlers
|
||||
|
||||
|
||||
<a name="v0.11.9"></a>
|
||||
## [v0.11.9] - 2019-11-22
|
||||
### Add
|
||||
- Add a db:reset command only for dev mode
|
||||
|
||||
|
||||
<a name="v0.11.8"></a>
|
||||
## [v0.11.8] - 2019-11-21
|
||||
### Optimize
|
||||
- Optimize db queries limit use of transactions
|
||||
|
||||
|
||||
<a name="v0.11.7"></a>
|
||||
## [v0.11.7] - 2019-11-19
|
||||
### Added
|
||||
- Added support for multi-root queries
|
||||
|
||||
|
||||
<a name="v0.11.6"></a>
|
||||
## [v0.11.6] - 2019-11-15
|
||||
### Fix
|
||||
- Fix issues with JWT auth
|
||||
- Fix bug with migration filename generation
|
||||
- Fix bug with migration file name
|
||||
|
||||
|
||||
<a name="v0.11.5"></a>
|
||||
## [v0.11.5] - 2019-11-10
|
||||
### Fix
|
||||
- Fix bug with migration template name
|
||||
|
||||
|
||||
<a name="v0.11.4"></a>
|
||||
## [v0.11.4] - 2019-11-10
|
||||
### Fix
|
||||
- Fix bug with creating new migrations
|
||||
|
||||
|
||||
<a name="v0.11.3"></a>
|
||||
## [v0.11.3] - 2019-11-09
|
||||
### Fix
|
||||
- Fix macro syntax bug in app templates
|
||||
|
||||
|
||||
<a name="v0.11.2"></a>
|
||||
## [v0.11.2] - 2019-11-07
|
||||
### Fix
|
||||
- Fix bugs and add new production mode
|
||||
|
||||
|
||||
<a name="v0.11.1"></a>
|
||||
## [v0.11.1] - 2019-11-05
|
||||
### Add
|
||||
- Add nested where clause to filter based on related tables
|
||||
|
||||
### Block
|
||||
- Block unauthorized requests when 'anon' role is not defined
|
||||
|
||||
### Update
|
||||
- Update docs and website with new features
|
||||
|
||||
|
||||
<a name="v0.11"></a>
|
||||
## [v0.11] - 2019-11-01
|
||||
### Add
|
||||
- Add config driven presets for insert, update and upsert
|
||||
- Add config driven presets for insert, update and upserta
|
||||
- Add RBAC option to disable functions eg. count
|
||||
- Add fuzz testing to 'serv' for the GQL hash parser
|
||||
- Add fuzz testing to 'jsn' and 'qcode'
|
||||
- Add ability to block queries and mutations by role
|
||||
- Add built in 'anon' and 'user' roles
|
||||
- Add role based access control
|
||||
|
||||
### Allow
|
||||
- Allow config files to inherit from other config files
|
||||
|
||||
### Change
|
||||
- Change config key inherit to inherits
|
||||
|
||||
### Get
|
||||
- Get RBAC working for queries and mutations
|
||||
|
||||
### Optimize
|
||||
- Optimize prepared statement flow for RBAC
|
||||
|
||||
### Preserve
|
||||
- Preserve allow.list ordering on save
|
||||
|
||||
### Update
|
||||
- Update filters section in guide
|
||||
|
||||
### Pull Requests
|
||||
- Merge pull request [#11](https://github.com/dosco/super-graph/issues/11) from dosco/rbac
|
||||
|
||||
|
||||
<a name="v0.10.1"></a>
|
||||
## [v0.10.1] - 2019-10-06
|
||||
### Add
|
||||
- Add ability to set filters per operation / action
|
||||
- Add upsert mutation
|
||||
|
||||
### Pull Requests
|
||||
- Merge pull request [#10](https://github.com/dosco/super-graph/issues/10) from FourSigma/sm-examples-folder
|
||||
|
||||
|
||||
<a name="v0.10"></a>
|
||||
## [v0.10] - 2019-10-04
|
||||
### Fix
|
||||
- Fix return values for bulk mutations and delete
|
||||
- Fix issues with mutation SQL
|
||||
- Fix broken demo app
|
||||
- Fix typo in 'across'
|
||||
|
||||
### Remove
|
||||
- Remove extra link from README
|
||||
|
||||
### Update
|
||||
- Update docs, getting started guide and mutations
|
||||
|
||||
### Pull Requests
|
||||
- Merge pull request [#6](https://github.com/dosco/super-graph/issues/6) from muesli/typo-fixes
|
||||
|
||||
|
||||
<a name="v0.9"></a>
|
||||
## [v0.9] - 2019-10-01
|
||||
### Fix
|
||||
- Fix demo rails app broken build
|
||||
|
||||
|
||||
<a name="v0.8"></a>
|
||||
## [v0.8] - 2019-09-30
|
||||
### Fix
|
||||
- Fix invalid import bug
|
||||
|
||||
### Update
|
||||
- Update documentation site
|
||||
|
||||
|
||||
<a name="v0.7"></a>
|
||||
## [v0.7] - 2019-09-29
|
||||
### Failure
|
||||
- Failure to prepare statements should be a warning
|
||||
|
||||
### Fix
|
||||
- Fix duplicte column bug
|
||||
|
||||
|
||||
<a name="v0.6"></a>
|
||||
## [v0.6] - 2019-09-29
|
||||
### Add
|
||||
- Add database setup commands
|
||||
- Add binary compression back to Dockerfile
|
||||
- Add initialization command to setup new apps
|
||||
- Add migrate command
|
||||
- Add database seeding capability
|
||||
- Add session variable for user id
|
||||
- Add delete mutation
|
||||
- Add update mutation
|
||||
- Add insert mutation with bulk insert
|
||||
- Add GoTO Aug, 19 presentation
|
||||
- Add support for prepared statements
|
||||
- Add end-to-end benchmaking
|
||||
- Add object pooling for parser expressions
|
||||
- Add request / response debugging for remote joins
|
||||
- Add a presentation about GraphQL
|
||||
- Add validation for remote JSON
|
||||
- Add tracing for API stitching
|
||||
- Add REST API stitching
|
||||
- Add SQL query cacheing
|
||||
- Add support for GraphQL variables
|
||||
- Add fuzz testing to qcode
|
||||
- Add test for Rails Redis cookie store integration
|
||||
- Add an install guide
|
||||
|
||||
### Change
|
||||
- Change fuzz test name to qcode
|
||||
- Change logo from PNG to SVG
|
||||
|
||||
### Enabke
|
||||
- Enabke reload on config change
|
||||
|
||||
### Fix
|
||||
- Fix missing config name bug
|
||||
- Fix new app templates
|
||||
- Fix help message for migrate
|
||||
- Fix session variable bug
|
||||
- Fix test failures in `psql` and `serv`
|
||||
- Fix demo docker services startup order
|
||||
- Fix wrong value for false token bug. Reported by [@ThisIsMissEm](https://github.com/ThisIsMissEm)
|
||||
- Fix allow.list file discovery bug
|
||||
- Fix bug with allow list path
|
||||
- Fix wrong value for use_allow_list in dev config
|
||||
- Fix startup bug in demo script
|
||||
- Fix url bug in allow list
|
||||
- Fix bug [#676](https://github.com/dosco/super-graph/issues/676) found by fuzzer
|
||||
- Fix race-condition in remote joins
|
||||
- Fix cookie passing in web ui
|
||||
- Fix bug with passing cookies in web ui
|
||||
- Fix null pointer with invalid argument values
|
||||
- Fix infinite loop bug in lexer
|
||||
- Fix null pointer issue found by fuzz test
|
||||
- Fix issue with fuzzbuzz config
|
||||
- Fix demo to run as memory only
|
||||
- Fix auth documentation
|
||||
- Fix issue with web ui sizing
|
||||
- Fix issue preventing docker-compose deploy
|
||||
- Fix try demo documentation
|
||||
|
||||
### Futher
|
||||
- Futher reduce allocations across hot paths
|
||||
- Futher reduce allocations on the compiler hot path
|
||||
- Futher optimize json parsing and editing performance
|
||||
|
||||
### Highlight
|
||||
- Highlight top features better on the site
|
||||
|
||||
### Improve
|
||||
- Improve readability of json parser code
|
||||
- Improve the motivation section in the readme
|
||||
- Improve the demo experience
|
||||
|
||||
### Make
|
||||
- Make remote joins use parallel http requests
|
||||
|
||||
### Merge
|
||||
- Merge branch 'master' into optimize-psql
|
||||
|
||||
### New
|
||||
- New low allocation fast json parsing and editing library
|
||||
|
||||
### Optimize
|
||||
- Optimize lexer and fix bugs
|
||||
- Optimize the sql generator hot path
|
||||
|
||||
### Reduce
|
||||
- Reduce alllocations done by the stack
|
||||
- Reduce steps to run the demo
|
||||
- Reduce allocations and improve perf over 50%
|
||||
|
||||
### Remove
|
||||
- Remove unused packages
|
||||
- Remove the 'hello' test app folder
|
||||
- Remove other allocations in psql
|
||||
|
||||
### Use
|
||||
- Use hash's as ids for table relationships
|
||||
|
||||
### Watch
|
||||
- Watch and reload on config changes
|
||||
|
||||
|
||||
<a name="v0.5"></a>
|
||||
## [v0.5] - 2019-04-10
|
||||
### Add
|
||||
- Add supprt for new Rails 5.2 aes-256-gcm cookies
|
||||
- Add query support for ts_rank and ts_headline
|
||||
- Add full text search support using TSV indexes
|
||||
- Add missing assets folder
|
||||
- Add fetch by ID feature
|
||||
- Add documentation
|
||||
|
||||
### Cleanup
|
||||
- Cleanup and redesign config files
|
||||
|
||||
### Fix
|
||||
- Fix bug with auth config parsing
|
||||
|
||||
### Redesign
|
||||
- Redesign config file architecture
|
||||
|
||||
### Reduce
|
||||
- Reduce realloc of maps and slices
|
||||
|
||||
### Update
|
||||
- Update docs with full-text search information
|
||||
|
||||
|
||||
<a name="v0.4"></a>
|
||||
## [v0.4] - 2019-04-01
|
||||
|
||||
<a name="v0.3"></a>
|
||||
## [v0.3] - 2019-04-01
|
||||
### Add
|
||||
- Add SQL execution timing and tracing
|
||||
- Add support for HAVING with aggregate queries
|
||||
- Add aggregrate functions to GQL queries
|
||||
- Add Auth0 JWT support
|
||||
- Add React UI building to the docker build flow
|
||||
- Add compiler profiling
|
||||
- Add bechmarks for GQL to SQL compile
|
||||
- Add tests for gql to sql compile
|
||||
|
||||
### Cleanup
|
||||
- Cleanup Dockerfile
|
||||
|
||||
### Fix
|
||||
- Fix recurring packer issue docker hub builds
|
||||
- Fix issue with asset packer breaking Docker builds
|
||||
- Fix missing git package in Dockerfile
|
||||
- Fix docker ignore values
|
||||
- Fix image build failure on docker hub
|
||||
- Fix build issue in Dockerfile
|
||||
- Fix bugs and document the 'where' clause
|
||||
- Fix perf issue with inflections
|
||||
|
||||
### Optimize
|
||||
- Optimize docker image
|
||||
|
||||
### Pack
|
||||
- Pack web UI with app into a single binary
|
||||
|
||||
### Upgrade
|
||||
- Upgrade web UI packages
|
||||
|
||||
|
||||
<a name="0.3"></a>
|
||||
## 0.3 - 2019-03-24
|
||||
### First
|
||||
- First commit
|
||||
|
||||
### Fix
|
||||
- Fix license to MIT
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/dosco/super-graph/compare/v0.12.4...HEAD
|
||||
[v0.12.4]: https://github.com/dosco/super-graph/compare/v0.12.3...v0.12.4
|
||||
[v0.12.3]: https://github.com/dosco/super-graph/compare/v0.12.2...v0.12.3
|
||||
[v0.12.2]: https://github.com/dosco/super-graph/compare/v0.12.1...v0.12.2
|
||||
[v0.12.1]: https://github.com/dosco/super-graph/compare/v0.12.0...v0.12.1
|
||||
[v0.12.0]: https://github.com/dosco/super-graph/compare/v0.11.9...v0.12.0
|
||||
[v0.11.9]: https://github.com/dosco/super-graph/compare/v0.11.8...v0.11.9
|
||||
[v0.11.8]: https://github.com/dosco/super-graph/compare/v0.11.7...v0.11.8
|
||||
[v0.11.7]: https://github.com/dosco/super-graph/compare/v0.11.6...v0.11.7
|
||||
[v0.11.6]: https://github.com/dosco/super-graph/compare/v0.11.5...v0.11.6
|
||||
[v0.11.5]: https://github.com/dosco/super-graph/compare/v0.11.4...v0.11.5
|
||||
[v0.11.4]: https://github.com/dosco/super-graph/compare/v0.11.3...v0.11.4
|
||||
[v0.11.3]: https://github.com/dosco/super-graph/compare/v0.11.2...v0.11.3
|
||||
[v0.11.2]: https://github.com/dosco/super-graph/compare/v0.11.1...v0.11.2
|
||||
[v0.11.1]: https://github.com/dosco/super-graph/compare/v0.11...v0.11.1
|
||||
[v0.11]: https://github.com/dosco/super-graph/compare/v0.10.1...v0.11
|
||||
[v0.10.1]: https://github.com/dosco/super-graph/compare/v0.10...v0.10.1
|
||||
[v0.10]: https://github.com/dosco/super-graph/compare/v0.9...v0.10
|
||||
[v0.9]: https://github.com/dosco/super-graph/compare/v0.8...v0.9
|
||||
[v0.8]: https://github.com/dosco/super-graph/compare/v0.7...v0.8
|
||||
[v0.7]: https://github.com/dosco/super-graph/compare/v0.6...v0.7
|
||||
[v0.6]: https://github.com/dosco/super-graph/compare/v0.5...v0.6
|
||||
[v0.5]: https://github.com/dosco/super-graph/compare/v0.4...v0.5
|
||||
[v0.4]: https://github.com/dosco/super-graph/compare/v0.3...v0.4
|
||||
[v0.3]: https://github.com/dosco/super-graph/compare/0.3...v0.3
|
79
CONTRIBUTING.md
Normal file
79
CONTRIBUTING.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Contributing to Super Graph
|
||||
|
||||
Super Graph is a very approchable code-base and a project that is easy for almost
|
||||
anyone with basic GO knowledge to start contributing to. It is also a young project
|
||||
so a lot of high value work is there for the taking.
|
||||
|
||||
Even the GraphQL to SQL compiler that is at the heart of Super Graph is essentially a text book compiler with clean and easy to read code. The data structures used by the lexer, parser and sql generator are easy to understand and modify.
|
||||
|
||||
Finally we do have a lot of test for critical parts of the codebase which makes it easy for you to modify with confidence. I'm always available for questions or any sort of guidance so feel fee to reach out over twitter or discord.
|
||||
|
||||
* [Getting Started](#get-started)
|
||||
* [Setting Up the Development Environment](#get-setup)
|
||||
* [Prerequisites](#prerequisites)
|
||||
* [Get the Super Graph source](#get-source)
|
||||
* [Start the development envoirnment ](#start-dev)
|
||||
* [Testing](#testing)
|
||||
* [Contributing](#contributing)
|
||||
* [Guidelines](#guidelines)
|
||||
* [Code style](#code-style)
|
||||
|
||||
## Getting Started {#get-started}
|
||||
|
||||
- Read the [Getting Started Guide](https://supergraph.dev/guide.html#get-started)
|
||||
|
||||
## Setting Up the Development Environment {#get-setup}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Install [Git](https://git-scm.com/) (may be already installed on your system, or available through your OS package manager)
|
||||
- Install [Go 1.13 or above](https://golang.org/doc/install)
|
||||
- Install [Docker](https://docs.docker.com/v17.09/engine/installation/)
|
||||
|
||||
### Get the Super Graph source {#get-source}
|
||||
|
||||
The entire build flow uses `Makefile` there is a whole list of sub-commands you
|
||||
can use to build, test, install, lint, etc.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dosco/super-graph
|
||||
cd ./super-graph
|
||||
make help
|
||||
```
|
||||
|
||||
### Start the development envoirnment {#start-dev}
|
||||
|
||||
The entire development flow is packaged into a `docker-compose` work flow. The below `up` command will launch A Postgres database, a example e-commerce app in Rails and Super Graph in development mode. The `db:seed` Rails task will insert sample data into Postgres.
|
||||
|
||||
```bash
|
||||
docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Learn how the code works
|
||||
|
||||
[Super Graph codebase explained](https://supergraph.dev/internals.html)
|
||||
|
||||
### Testing and Linting {#testing}
|
||||
|
||||
```
|
||||
make lint test
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
- **Pull requests are welcome**, as long as you're willing to put in the effort to meet the guidelines.
|
||||
- Aim for clear, well written, maintainable code.
|
||||
- Simple and minimal approach to features, like Go.
|
||||
- Refactoring existing code now for better performance, better readability or better testability wins over adding a new feature.
|
||||
- Don't add a function to a module that you don't use right now, or doesn't clearly enable a planned functionality.
|
||||
- Don't ship a half done feature, which would require significant alterations to work fully.
|
||||
- Avoid [Technical debt](https://en.wikipedia.org/wiki/Technical_debt) like cancer.
|
||||
- Leave the code cleaner than when you began.
|
||||
|
||||
### Code style
|
||||
- We're following [Go Code Review](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
- Use `go fmt` to format your code before committing.
|
||||
- If you see *any code* which clearly violates the style guide, please fix it and send a pull request. No need to ask for permission.
|
||||
- Avoid unnecessary vertical spaces. Use your judgment or follow the code review comments.
|
||||
- Wrap your code and comments to 100 characters, unless doing so makes the code less legible.
|
12
Dockerfile
12
Dockerfile
@ -6,13 +6,13 @@ RUN yarn
|
||||
RUN yarn build
|
||||
|
||||
# stage: 2
|
||||
FROM golang:1.13beta1-alpine as go-build
|
||||
FROM golang:1.13.4-alpine as go-build
|
||||
RUN apk update && \
|
||||
apk add --no-cache make && \
|
||||
apk add --no-cache git && \
|
||||
apk add --no-cache upx=3.95-r2
|
||||
|
||||
RUN go get -u github.com/rafaelsq/wtc && \
|
||||
go get -u github.com/GeertJohan/go.rice/rice
|
||||
RUN GO111MODULE=off go get -u github.com/rafaelsq/wtc
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
@ -20,11 +20,9 @@ COPY . /app
|
||||
RUN mkdir -p /app/web/build
|
||||
COPY --from=react-build /web/build/ ./web/build/
|
||||
|
||||
ENV GO111MODULE=on
|
||||
RUN go mod vendor
|
||||
RUN go generate ./... && \
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o super-graph && \
|
||||
echo "Compressing binary, will take a bit of time..." && \
|
||||
RUN make build
|
||||
RUN echo "Compressing binary, will take a bit of time..." && \
|
||||
upx --ultra-brute -qq super-graph && \
|
||||
upx -t super-graph
|
||||
|
||||
|
189
LICENSE
189
LICENSE
@ -1,21 +1,176 @@
|
||||
The MIT License (MIT)
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2019-present Vikram Rangnekar. twitter.com/dosco
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
103
Makefile
Normal file
103
Makefile
Normal file
@ -0,0 +1,103 @@
|
||||
BUILD ?= $(shell git rev-parse --short HEAD)
|
||||
BUILD_DATE ?= $(shell git log -1 --format=%ci)
|
||||
BUILD_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
BUILD_VERSION ?= $(shell git describe --always --tags)
|
||||
|
||||
GOPATH ?= $(shell go env GOPATH)
|
||||
|
||||
ifndef GOPATH
|
||||
override GOPATH = $(HOME)/go
|
||||
endif
|
||||
|
||||
export GO111MODULE := on
|
||||
|
||||
# Build-time Go variables
|
||||
version = github.com/dosco/super-graph/serv.version
|
||||
gitBranch = github.com/dosco/super-graph/serv.gitBranch
|
||||
lastCommitSHA = github.com/dosco/super-graph/serv.lastCommitSHA
|
||||
lastCommitTime = github.com/dosco/super-graph/serv.lastCommitTime
|
||||
|
||||
BUILD_FLAGS ?= -ldflags '-s -w -X ${lastCommitSHA}=${BUILD} -X "${lastCommitTime}=${BUILD_DATE}" -X "${version}=${BUILD_VERSION}" -X ${gitBranch}=${BUILD_BRANCH}'
|
||||
|
||||
.PHONY: all build gen clean test run lint changlog release version help $(PLATFORMS)
|
||||
|
||||
test:
|
||||
@go test -v ./...
|
||||
|
||||
BIN_DIR := $(GOPATH)/bin
|
||||
GORICE := $(BIN_DIR)/github.com/GeertJohan/go.rice
|
||||
GOLANGCILINT := $(BIN_DIR)/golangci-lint
|
||||
GITCHGLOG := $(BIN_DIR)/git-chglog
|
||||
|
||||
$(GORICE):
|
||||
@GO111MODULE=off go get -u github.com/GeertJohan/go.rice/rice
|
||||
|
||||
$(GITCHGLOG):
|
||||
@GO111MODULE=off go get -u github.com/git-chglog/git-chglog/cmd/git-chglog
|
||||
|
||||
changelog: $(GITCHGLOG)
|
||||
@git-chglog $(ARGS)
|
||||
|
||||
$(GOLANGCILINT):
|
||||
@GO111MODULE=off curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(GOPATH)/bin v1.21.0
|
||||
|
||||
lint: $(GOMETALINTER)
|
||||
@golangci-lint run ./... --skip-dirs-use-default
|
||||
|
||||
BINARY := super-graph
|
||||
LDFLAGS := -s -w
|
||||
PLATFORMS := windows linux darwin
|
||||
os = $(word 1, $@)
|
||||
|
||||
$(PLATFORMS): lint test gen
|
||||
@mkdir -p release
|
||||
@GOOS=$(os) GOARCH=amd64 go build $(BUILD_FLAGS) -o release/$(BINARY)-$(BUILD_VERSION)-$(os)-amd64
|
||||
|
||||
release: windows linux darwin
|
||||
|
||||
all: lint test $(BINARY)
|
||||
|
||||
build: $(BINARY)
|
||||
|
||||
gen: $(GORICE)
|
||||
@go generate ./...
|
||||
|
||||
$(BINARY): clean gen
|
||||
@go build $(BUILD_FLAGS) -o $(BINARY)
|
||||
|
||||
clean:
|
||||
@rm -f $(BINARY)
|
||||
|
||||
run: clean
|
||||
@go run $(BUILD_FLAGS) main.go $(ARGS)
|
||||
|
||||
install: gen
|
||||
@echo $(GOPATH)
|
||||
@echo "Commit Hash: `git rev-parse HEAD`"
|
||||
@echo "Old Hash: `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`"
|
||||
@go install $(BUILD_FLAGS)
|
||||
@echo "New Hash:" `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`
|
||||
|
||||
uninstall: clean
|
||||
@go clean -i -x
|
||||
|
||||
version:
|
||||
@echo Super Graph ${BUILD_VERSION}
|
||||
@echo Build: ${BUILD}
|
||||
@echo Build date: ${BUILD_DATE}
|
||||
@echo Branch: ${BUILD_BRANCH}
|
||||
@echo Go version: $(shell go version)
|
||||
|
||||
help:
|
||||
@echo
|
||||
@echo Build commands:
|
||||
@echo " make build - Build supergraph binary"
|
||||
@echo " make install - Install supergraph binary"
|
||||
@echo " make uninstall - Uninstall supergraph binary"
|
||||
@echo " make [platform] - Build for platform [linux|darwin|windows]"
|
||||
@echo " make release - Build all platforms"
|
||||
@echo " make run - Run supergraph (eg. make run ARGS=\"help\")"
|
||||
@echo " make test - Run all tests"
|
||||
@echo " make changelog - Generate changelog (eg. make changelog ARGS=\"help\")"
|
||||
@echo " make help - This help"
|
||||
@echo
|
13
NOTICE
Normal file
13
NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
Copyright 2019 Vikram Rangnekar
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
32
README.md
32
README.md
@ -1,16 +1,24 @@
|
||||
<a href="https://supergraph.dev"><img src="https://supergraph.dev/hologram.svg" width="100" height="100" align="right" /></a>
|
||||
<!-- <a href="https://supergraph.dev"><img src="https://supergraph.dev/hologram.svg" width="100" height="100" align="right" /></a> -->
|
||||
|
||||
# Super Graph - Instant GraphQL APIs for your apps.
|
||||
<img src="docs/.vuepress/public/super-graph.png" width="250" />
|
||||
|
||||
## Build web products faster. No code needed. GraphQL auto. transformed into efficient database queries.
|
||||
### Build web products faster. Secure high performance GraphQL
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/6pSWCTZ)
|
||||
|
||||
|
||||
## What is Super Graph
|
||||
|
||||
Super Graph is a micro-service that instantly and without code gives you a high performance and secure GraphQL API. Your GraphQL queries are auto translated into a single fast SQL query. No more writing API code as you develop your web frontend just make the query you need and Super Graph will do the rest.
|
||||
|
||||
Super Graph has a rich feature set like integrating with your existing Ruby on Rails apps, joining your DB with data from remote APIs, Role and Attribute based access control, Supoport for JWT tokens, Built-in DB mutations and seeding and a lot more.
|
||||
|
||||

|
||||
|
||||
|
||||
## The story of Super Graph?
|
||||
|
||||
After working on several products through my career I find that we spend way too much time on building API backends. Most APIs also require constant updating, this costs real time and money.
|
||||
@ -25,8 +33,8 @@ This compiler is what sits at the heart of Super Graph with layers of useful fun
|
||||
|
||||
## Features
|
||||
|
||||
- Role based access control
|
||||
- Works with Ruby-On-Rails databases
|
||||
- Role and Attribute based access control
|
||||
- Works with existing Ruby-On-Rails apps
|
||||
- Automatically learns database schemas and relationships
|
||||
- Full text search and aggregations
|
||||
- Rails authentication supported (Redis, Memcache, Cookie)
|
||||
@ -41,6 +49,16 @@ This compiler is what sits at the heart of Super Graph with layers of useful fun
|
||||
- Database migrations tool
|
||||
- Database seeding tool
|
||||
|
||||
## Get started
|
||||
|
||||
```
|
||||
git clone https://github.com/dosco/super-graph
|
||||
cd ./super-graph
|
||||
make install
|
||||
|
||||
super-graph new <app_name>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
[supergraph.dev](https://supergraph.dev)
|
||||
@ -56,7 +74,7 @@ Twitter or Discord.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](http://opensource.org/licenses/MIT)
|
||||
[Apache Public License 2.0](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
Copyright (c) 2019-present Vikram Rangnekar
|
||||
|
||||
|
@ -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 {
|
||||
products {
|
||||
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 {
|
||||
products {
|
||||
id
|
||||
@ -174,39 +122,6 @@ mutation {
|
||||
}
|
||||
}
|
||||
|
||||
variables {
|
||||
"data": [
|
||||
{
|
||||
"name": "Gumbo1",
|
||||
"created_at": "now",
|
||||
"updated_at": "now"
|
||||
},
|
||||
{
|
||||
"name": "Gumbo2",
|
||||
"created_at": "now",
|
||||
"updated_at": "now"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
query {
|
||||
products {
|
||||
id
|
||||
name
|
||||
users {
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query {
|
||||
me {
|
||||
id
|
||||
email
|
||||
full_name
|
||||
}
|
||||
}
|
||||
|
||||
variables {
|
||||
"update": {
|
||||
"name": "Helloo",
|
||||
@ -223,70 +138,6 @@ mutation {
|
||||
}
|
||||
}
|
||||
|
||||
variables {
|
||||
"data": [
|
||||
{
|
||||
"name": "Gumbo1",
|
||||
"created_at": "now",
|
||||
"updated_at": "now"
|
||||
},
|
||||
{
|
||||
"name": "Gumbo2",
|
||||
"created_at": "now",
|
||||
"updated_at": "now"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
query {
|
||||
product {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
variables {
|
||||
"data": [
|
||||
{
|
||||
"name": "Gumbo1",
|
||||
"created_at": "now",
|
||||
"updated_at": "now"
|
||||
},
|
||||
{
|
||||
"name": "Gumbo2",
|
||||
"created_at": "now",
|
||||
"updated_at": "now"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
query {
|
||||
products {
|
||||
id
|
||||
name
|
||||
description
|
||||
users {
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query {
|
||||
users {
|
||||
id
|
||||
email
|
||||
picture: avatar
|
||||
password
|
||||
full_name
|
||||
products(limit: 2, where: {price: {gt: 10}}) {
|
||||
id
|
||||
name
|
||||
description
|
||||
price
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variables {
|
||||
"data": {
|
||||
"name": "WOOO",
|
||||
@ -301,4 +152,20 @@ mutation {
|
||||
}
|
||||
}
|
||||
|
||||
query getProducts {
|
||||
products {
|
||||
id
|
||||
name
|
||||
price
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
query {
|
||||
deals {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,18 +101,14 @@ database:
|
||||
variables:
|
||||
admin_account_id: "5"
|
||||
|
||||
# Define defaults to for the field key and values below
|
||||
defaults:
|
||||
# filters: ["{ user_id: { eq: $user_id } }"]
|
||||
|
||||
# Field and table names that you wish to block
|
||||
blocklist:
|
||||
- ar_internal_metadata
|
||||
- schema_migrations
|
||||
- secret
|
||||
- password
|
||||
- encrypted
|
||||
- token
|
||||
# Field and table names that you wish to block
|
||||
blocklist:
|
||||
- ar_internal_metadata
|
||||
- schema_migrations
|
||||
- secret
|
||||
- password
|
||||
- encrypted
|
||||
- token
|
||||
|
||||
tables:
|
||||
- name: customers
|
||||
@ -135,15 +131,17 @@ tables:
|
||||
name: me
|
||||
table: users
|
||||
|
||||
- name: deals
|
||||
table: products
|
||||
|
||||
roles_query: "SELECT * FROM users WHERE id = $user_id"
|
||||
|
||||
roles:
|
||||
- name: anon
|
||||
tables:
|
||||
- name: products
|
||||
limit: 10
|
||||
|
||||
query:
|
||||
limit: 10
|
||||
columns: ["id", "name", "description" ]
|
||||
aggregation: false
|
||||
|
||||
@ -156,6 +154,13 @@ roles:
|
||||
delete:
|
||||
block: false
|
||||
|
||||
- name: deals
|
||||
|
||||
query:
|
||||
limit: 3
|
||||
columns: ["name", "description" ]
|
||||
aggregation: false
|
||||
|
||||
- name: user
|
||||
tables:
|
||||
- name: users
|
||||
|
@ -8,15 +8,11 @@
|
||||
<div class="bg-bottom bg-no-repeat bg-cover">
|
||||
<div class="text-center md:text-left pt-24">
|
||||
<h1 v-if="data.heroText !== null" class="text-5xl font-bold text-black pb-0 uppercase">
|
||||
{{ data.heroText || $title || 'Hello' }}
|
||||
<img src="/super-graph.png" width="250" />
|
||||
</h1>
|
||||
|
||||
<p class="text-2xl text-gray-700 leading-tight pb-0">
|
||||
{{ data.tagline || $description || 'Welcome to your VuePress site' }}
|
||||
</p>
|
||||
|
||||
<p class="text-lg text-gray-600 leading-tight">
|
||||
{{ data.longTagline }}
|
||||
<p class="text-4xl text-gray-800 leading-tight mt-1">
|
||||
Build web products faster. Secure high performance GraphQL
|
||||
</p>
|
||||
|
||||
<NavLink
|
||||
|
@ -12,10 +12,10 @@ module.exports = {
|
||||
nav: [
|
||||
{ text: 'Docs', link: '/guide' },
|
||||
{ text: 'Deploy', link: '/deploy' },
|
||||
{ text: 'Internals', link: '/internals' },
|
||||
{ text: 'Github', link: 'https://github.com/dosco/super-graph' },
|
||||
{ text: 'Docker', link: 'https://hub.docker.com/r/dosco/super-graph/builds' },
|
||||
{ text: 'Join Chat', link: 'https://discord.gg/NKdXBc' },
|
||||
|
||||
],
|
||||
serviceWorker: {
|
||||
updatePopup: true
|
||||
|
BIN
docs/.vuepress/public/super-graph.png
Normal file
BIN
docs/.vuepress/public/super-graph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -26,5 +26,5 @@ features:
|
||||
- title: Free and Open Source
|
||||
details: Not a VC funded startup. Not even a startup just good old open source code
|
||||
|
||||
footer: MIT Licensed | Copyright © 2018-present Vikram Rangnekar
|
||||
footer: Apache Public License 2.0 | Copyright © 2018-present Vikram Rangnekar
|
||||
---
|
||||
|
@ -4,13 +4,15 @@ sidebar: auto
|
||||
|
||||
# Guide to Super Graph
|
||||
|
||||
Get an instant high performance GraphQL API for Postgres. No code needed. GraphQL is automatically transformed into efficient database queries. Also Designed to integrate with your Rails apps.
|
||||
Super Graph is a micro-service that instantly and without code gives you a high performance and secure GraphQL API. Your GraphQL queries are auto translated into a single fast SQL query. No more writing API code as you develop your web frontend just make the query you need and Super Graph will do the rest.
|
||||
|
||||
Super Graph has a rich feature set like integrating with your existing Ruby on Rails apps, joining your DB with data from remote APIs, Role and Attribute based access control, Supoport for JWT tokens, DB migrations, seeding and a lot more.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Role based access control
|
||||
- Works with Ruby-On-Rails databases
|
||||
- Role and Attribute based access control
|
||||
- Works with existing Ruby-On-Rails apps
|
||||
- Automatically learns database schemas and relationships
|
||||
- Full text search and aggregations
|
||||
- Rails authentication supported (Redis, Memcache, Cookie)
|
||||
@ -25,6 +27,7 @@ Get an instant high performance GraphQL API for Postgres. No code needed. GraphQ
|
||||
- Database migrations tool
|
||||
- Database seeding tool
|
||||
|
||||
|
||||
## Try the demo app
|
||||
|
||||
```bash
|
||||
@ -147,9 +150,13 @@ Super Graph can generate your initial app for you. The generated app will have c
|
||||
You can then add your database schema to the migrations, maybe create some seed data using the seed script and launch Super Graph. You're now good to go and can start working on your UI frontend in React, Vue or whatever.
|
||||
|
||||
```bash
|
||||
# use the below command to download and install Super Graph. You will need Go 1.13 or above
|
||||
GO111MODULE=on go get -u github.com/dosco/super-graph
|
||||
# Download and install Super Graph. You will need Go 1.13 or above
|
||||
git clone https://github.com/dosco/super-graph && cd super-graph && make install
|
||||
```
|
||||
|
||||
And then create and launch you're new app
|
||||
|
||||
```bash
|
||||
# create a new app and change to it's directory
|
||||
super-graph new blog; cd blog
|
||||
|
||||
@ -1275,18 +1282,14 @@ database:
|
||||
variables:
|
||||
admin_account_id: "5"
|
||||
|
||||
# Define defaults to for the field key and values below
|
||||
defaults:
|
||||
# filters: ["{ user_id: { eq: $user_id } }"]
|
||||
|
||||
# Field and table names that you wish to block
|
||||
blocklist:
|
||||
- ar_internal_metadata
|
||||
- schema_migrations
|
||||
- secret
|
||||
- password
|
||||
- encrypted
|
||||
- token
|
||||
# Field and table names that you wish to block
|
||||
blocklist:
|
||||
- ar_internal_metadata
|
||||
- schema_migrations
|
||||
- secret
|
||||
- password
|
||||
- encrypted
|
||||
- token
|
||||
|
||||
tables:
|
||||
- name: customers
|
||||
@ -1400,9 +1403,6 @@ brew install yarn
|
||||
# yarn install dependencies and build the web ui
|
||||
(cd web && yarn install && yarn build)
|
||||
|
||||
# generate some stuff the go code needs
|
||||
go generate ./...
|
||||
|
||||
# do this the only the time to setup the database
|
||||
docker-compose run rails_app rake db:create db:migrate db:seed
|
||||
|
||||
@ -1411,6 +1411,10 @@ docker-compose up
|
||||
|
||||
```
|
||||
|
||||
## MIT License
|
||||
## Learn how the code works
|
||||
|
||||
MIT Licensed | Copyright © 2018-present Vikram Rangnekar
|
||||
[Super Graph codebase explained](https://supergraph.dev/internals.html)
|
||||
|
||||
## Apache License 2.0
|
||||
|
||||
Apache Public License 2.0 | Copyright © 2018-present Vikram Rangnekar
|
||||
|
241
docs/internals.md
Normal file
241
docs/internals.md
Normal file
@ -0,0 +1,241 @@
|
||||
---
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# Super Graph Codebase Explained
|
||||
|
||||
Super Graph code is made up of a number of packages. We have done our best to keep each package small and focused. Let us begin by looking at some of these packages.
|
||||
|
||||
1. qcode - GraphQL lexer and parser.
|
||||
2. psql - SQL generator
|
||||
3. serv - HTTP Endpoint, Configs, CLI, etc
|
||||
4. rails - Rails cookie and session store decoders
|
||||
|
||||
## QCODE
|
||||
|
||||
This package contains the core of the GraphQL conpiler it handling the lexing and parsing of the GraphQL query transforming it into an internal representation called
|
||||
`QCode`.
|
||||
|
||||
This is the first step of the compiling process the `func NewCompiler(c Config)` function creates a new instance of this compiler which has it's own config.
|
||||
|
||||
Keep in mind QCode has no knowledge of the Database structure it is designed to be a fast GraphQL parser. Care is taken to keep memory allocations to a minimum.
|
||||
|
||||
```go
|
||||
const (
|
||||
opQuery
|
||||
opMutate
|
||||
...
|
||||
)
|
||||
|
||||
type QCode struct {
|
||||
Type QType
|
||||
Selects []Select
|
||||
...
|
||||
}
|
||||
|
||||
type Select struct {
|
||||
ID int32
|
||||
ParentID int32
|
||||
Args map[string]*Node
|
||||
Name string
|
||||
FieldName string
|
||||
Cols []Column
|
||||
Where *Exp
|
||||
OrderBy []*OrderBy
|
||||
DistinctOn []string
|
||||
Paging Paging
|
||||
Children []int32
|
||||
Functions bool
|
||||
Allowed map[string]struct{}
|
||||
PresetMap map[string]string
|
||||
PresetList []string
|
||||
}
|
||||
```
|
||||
|
||||
But before the incoming GraphQL query can be turned into QCode it must first be tokenzied by the lexer `lex.go`. As the tokenzier walks the bytes of the query it generates tokens `item` structs which are then consumed by the next step the parser `parse.go`.
|
||||
|
||||
```go
|
||||
type item struct {
|
||||
typ itemType
|
||||
pos Pos
|
||||
end Pos
|
||||
}
|
||||
```
|
||||
|
||||
For exmple a simple query like `query getUser { user { id } }` will be converted into several tokens like below.
|
||||
|
||||
```go
|
||||
item{itemQuery, 0, 4} // query
|
||||
item{itemName, 6, 12} // getUser
|
||||
item{itemObjOpen, 16, 20} // {
|
||||
...
|
||||
```
|
||||
|
||||
These tokens are then fed into the parser `parse.go` the parser does the work of generating an abstract syntax tree (AST) from the tokens. This AST is an internal representation (data structure) and is not exposed outside the package. Sinc the AST is a tree a stack `stack.go` is used to walk the tree and generate the QCode AST. The QCode data structure is also a tree (represented as an array). This is then returned to the caller of the compile function.
|
||||
|
||||
```go
|
||||
type Operation struct {
|
||||
Type parserType
|
||||
Name string
|
||||
Args []Arg
|
||||
Fields []Field
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
ID int32
|
||||
ParentID int32
|
||||
Name string
|
||||
Alias string
|
||||
Args []Arg
|
||||
Children []int32
|
||||
}
|
||||
```
|
||||
|
||||
## PSQL
|
||||
|
||||
This package is responsible for generating Postgres SQL from the QCode AST. There are various GraphQL query types (Query, Mutation, etc). And several more sub types like single root or multi-root queries, various types of mutations (insert, update delete, bulk insert, etc). This package is designed to be able to generate SQL for all of those types.
|
||||
|
||||
In addition to QCode variable data is also passed to the compile function within this package. Variables are decoded to derive what is being inserted and what kind of insert is it single or bulk. This information is not available in the GraphQL query its passed in seperatly via variables. This package is able to put all this together and generate the right SQL code.
|
||||
|
||||
The entry point of this package is in `query.go`. The database schema must be passed in the config object when creating a new compiler instance `NewCompiler`. The functions to extract this schema from the database are also part of this package `tables.go`. The `GetTables` functions fetches all the tables from the database and `GetColumns` fetches columns and relationship information.
|
||||
|
||||
```go
|
||||
func NewCompiler(conf Config) *Compiler {
|
||||
return &Compiler{conf.Schema, conf.Vars}
|
||||
}
|
||||
|
||||
func (co *Compiler) Compile(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
|
||||
switch qc.Type {
|
||||
case qcode.QTQuery:
|
||||
return co.compileQuery(qc, w)
|
||||
case qcode.QTInsert, qcode.QTUpdate, qcode.QTDelete, qcode.QTUpsert:
|
||||
return co.compileMutation(qc, w, vars)
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("Unknown operation type %d", qc.Type)
|
||||
}
|
||||
```
|
||||
|
||||
GraphQL, input is first converted to QCode.
|
||||
|
||||
```graphql
|
||||
query {
|
||||
user {
|
||||
id
|
||||
}
|
||||
posts {
|
||||
title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
SQL, in reality the generated SQL is far more complex single it has to be very efficient, leverage the power of Postgres, support RBAC (Role based access control) and all of this must be done in a single SQL query.
|
||||
|
||||
```sql
|
||||
SELECT users.id, posts.title FROM users, posts;
|
||||
```
|
||||
|
||||
## SERV
|
||||
|
||||
The `serv` package constains most of code that turns the above compiler into an HTTP service. It also includes authentication middleware, remote join resolvers, config parsering, database migrations and seeding commands.
|
||||
|
||||
Another big feature that this package handles is the `allow.list` management code. In production mode parsing the allow list file and registering prepared statements to adding GraphQL queries to this file in development mode.
|
||||
|
||||
Currently the following global variables are referrenced across the package. In future I'd prefer to move these into a context struct and pass that around instead.
|
||||
|
||||
```go
|
||||
var (
|
||||
logger zerolog.Logger // logger for everything but errors
|
||||
errlog zerolog.Logger // logger for errors includes line numbers
|
||||
conf *config // parsed config
|
||||
confPath string // path to the config file
|
||||
db *pgxpool.Pool // database connection pool
|
||||
schema *psql.DBSchema // database tables, columns and relationships
|
||||
qcompile *qcode.Compiler // qcode compiler
|
||||
pcompile *psql.Compiler // postgres sql compiler
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
There are several unit tests and benchmark tests `parse_test.go`) included. There are also scripts included for memory `pprof_cpu.sh` and cpu `pprof_cpu.sh` profiling.
|
||||
|
||||
```go
|
||||
// Test to ensure synthetic tables gnerate the correct SQL
|
||||
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")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(resSQL) != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can run tests within each package or across the entire app. It is usually the fastest to first write a test and then build the feature to satisfy it.
|
||||
|
||||
```
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
Memory profiling can help find where allocations are happining within the package code.
|
||||
|
||||
```bash
|
||||
$ cd ./psql
|
||||
$ ./pprof_mem.sh
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
pkg: github.com/dosco/super-graph/psql
|
||||
BenchmarkCompile-8 52567 19401 ns/op 3918 B/op 61 allocs/op
|
||||
BenchmarkCompileParallel-8 219548 5684 ns/op 3938 B/op 61 allocs/op
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/psql 2.582s
|
||||
Type: alloc_space
|
||||
Time: Nov 29, 2019 at 11:59pm (EST)
|
||||
Entering interactive mode (type "help" for commands, "o" for options)
|
||||
(pprof) top
|
||||
Showing nodes accounting for 880.59MB, 80.63% of 1092.14MB total
|
||||
Dropped 33 nodes (cum <= 5.46MB)
|
||||
Showing top 10 nodes out of 35
|
||||
flat flat% sum% cum cum%
|
||||
22MB 2.01% 2.01% 903.57MB 82.73% github.com/dosco/super-graph/qcode.(*Compiler).Compile
|
||||
0 0% 2.01% 862.98MB 79.02% github.com/dosco/super-graph/psql.BenchmarkCompileParallel.func1
|
||||
0 0% 2.01% 862.98MB 79.02% testing.(*B).RunParallel.func1
|
||||
461.95MB 42.30% 44.31% 760.53MB 69.64% github.com/dosco/super-graph/qcode.(*Compiler).compileQuery
|
||||
396.63MB 36.32% 80.63% 396.63MB 36.32% github.com/dosco/super-graph/util.NewStack
|
||||
0 0% 80.63% 252.07MB 23.08% github.com/dosco/super-graph/qcode.(*Compiler).compileArgs
|
||||
0 0% 80.63% 228.15MB 20.89% testing.(*B).runN
|
||||
0 0% 80.63% 227.63MB 20.84% github.com/dosco/super-graph/psql.BenchmarkCompile
|
||||
0 0% 80.63% 227.63MB 20.84% testing.(*B).launch
|
||||
0 0% 80.63% 187.04MB 17.13% github.com/dosco/super-graph/psql.(*Compiler).Compile
|
||||
```
|
||||
|
||||
## Benchmarking
|
||||
|
||||
Most packages contain benchmark tests to ensure new features don't introduce a significant regression to performance.
|
||||
|
||||
```bash
|
||||
$ cd ./psql
|
||||
$ go test -v -run=xx -bench=.
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
pkg: github.com/dosco/super-graph/psql
|
||||
BenchmarkCompile-8 60775 19076 ns/op 3919 B/op 61 allocs/op
|
||||
BenchmarkCompileParallel-8 207847 5172 ns/op 3937 B/op 61 allocs/op
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/psql 2.530s
|
||||
```
|
||||
|
||||
## Reach out
|
||||
|
||||
If you'd like me to explain other parts of the code please reach out over Twitter or Discord. I'll keep adding to this doc as I get time.
|
12
go.mod
12
go.mod
@ -3,7 +3,6 @@ module github.com/dosco/super-graph
|
||||
require (
|
||||
github.com/GeertJohan/go.rice v1.0.0
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/OneOfOne/xxhash v1.2.5 // indirect
|
||||
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||
github.com/brianvoe/gofakeit v3.18.0+incompatible
|
||||
@ -12,32 +11,25 @@ require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/dlclark/regexp2 v1.2.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/garyburd/redigo v1.6.0
|
||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
|
||||
github.com/gobuffalo/flect v0.1.6
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
|
||||
github.com/jackc/pgconn v1.0.1
|
||||
github.com/jackc/pgtype v1.0.1
|
||||
github.com/jackc/pgx v3.6.0+incompatible
|
||||
github.com/jackc/pgx/v4 v4.0.1
|
||||
github.com/jackc/tern v1.8.2
|
||||
github.com/magiconair/properties v1.8.1 // indirect
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/rs/zerolog v1.15.0
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/valyala/fasttemplate v1.0.1
|
||||
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
|
||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.7 // indirect
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
45
go.sum
45
go.sum
@ -1,34 +1,28 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI=
|
||||
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 h1:+qz9Ga6l6lKw6fgvk5RMV5HQznSLvI8Zxajwdj4FhFg=
|
||||
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3/go.mod h1:FlkD11RtgMTYjVuBnb7cxoHmQGqvPpCsr2atC88nl/M=
|
||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8=
|
||||
github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.0.0 h1:Eb1IiuHmi3FhT12NKfqCQXSXRqc4NTMvgJoREemrSt4=
|
||||
github.com/cespare/xxhash/v2 v2.0.0/go.mod h1:MaMeaVDXZNmTpkOyhVs3/WfjgobkbQgfrVnrr3DyZL0=
|
||||
github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
|
||||
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
@ -41,6 +35,7 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
|
||||
@ -57,8 +52,6 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw=
|
||||
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681 h1:3WV5aRRj1ELP3RcLlBp/v0WJTuy47OQMkL9GIQq8QEE=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||
@ -70,8 +63,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=
|
||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobuffalo/flect v0.1.1 h1:GTZJjJufv9FxgRs1+0Soo3wj+Md3kTUmTER/YE4uINA=
|
||||
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
|
||||
github.com/gobuffalo/flect v0.1.6 h1:D7KWNRFiCknJKA495/e1BO7oxqf8tbieaLv/ehoZ/+g=
|
||||
github.com/gobuffalo/flect v0.1.6/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
@ -87,8 +78,6 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
@ -100,8 +89,6 @@ github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZb
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0 h1:DUwgMQuuPnS0rhMXenUtZpqZqrR/30NWY+qQvTpSvEs=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
@ -126,9 +113,6 @@ github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCM
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.0.1 h1:7GWB9n3DdnO3TIbj59wMAE9QcHPL4cy/Bbtk5P1Noow=
|
||||
github.com/jackc/pgtype v1.0.1/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgx v0.0.0-20180217033919-55ca9db5d578/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jackc/pgx v3.6.0+incompatible h1:bJeo4JdVbDAW8KB2m8XkFeo8CPipREoG37BwEoKGz+Q=
|
||||
github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
@ -138,8 +122,7 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.0.0 h1:rbjAshlgKscNa7j0jAM0uNQflis5o2XUogPMVAwtcsM=
|
||||
github.com/jackc/puddle v1.0.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/tern v1.8.2 h1:+d9eK83fRS0dbf6nt+2tjILYF4FKG1O5xTFB8Lzc66U=
|
||||
github.com/jackc/tern v1.8.2/go.mod h1:AMppp2oyCT6rYnJHLLMmPWwahfFvdIVi6mr9gH81Nxs=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
@ -163,23 +146,25 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -199,6 +184,7 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
@ -208,22 +194,16 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.0-20160114030619-9c9300901990/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v0.0.0-20151218134703-7f60f83a2c81/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@ -247,8 +227,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks=
|
||||
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
@ -258,7 +236,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20151201002508-7b85b097bf75/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@ -303,8 +280,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU=
|
||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
@ -335,4 +312,6 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -139,7 +139,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error {
|
||||
}
|
||||
|
||||
if sk > 0 && sk < len(cb) {
|
||||
_, err = w.Write(cb[sk:len(cb)])
|
||||
_, err = w.Write(cb[sk:])
|
||||
} else {
|
||||
_, err = w.Write(cb)
|
||||
}
|
||||
|
@ -55,6 +55,6 @@ func TestFuzzCrashers(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, f := range crashers {
|
||||
unifiedTest([]byte(f))
|
||||
_ = unifiedTest([]byte(f))
|
||||
}
|
||||
}
|
||||
|
@ -191,11 +191,11 @@ func TestGet(t *testing.T) {
|
||||
}
|
||||
|
||||
for i := range expected {
|
||||
if bytes.Equal(values[i].Key, expected[i].Key) == false {
|
||||
if !bytes.Equal(values[i].Key, expected[i].Key) {
|
||||
t.Error(string(values[i].Key), " != ", string(expected[i].Key))
|
||||
}
|
||||
|
||||
if bytes.Equal(values[i].Value, expected[i].Value) == false {
|
||||
if !bytes.Equal(values[i].Value, expected[i].Value) {
|
||||
t.Error(string(values[i].Value), " != ", string(expected[i].Value))
|
||||
}
|
||||
}
|
||||
@ -225,7 +225,10 @@ func TestValue(t *testing.T) {
|
||||
|
||||
func TestFilter1(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
Filter(&b, []byte(input2), []string{"id", "full_name", "embed"})
|
||||
err := Filter(&b, []byte(input2), []string{"id", "full_name", "embed"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
expected := `[{"id": 1,"full_name": "Sidney Stroman","embed": {"id": 8,"full_name": "Caroll Orn Sr.","email": "joannarau@hegmann.io","__twitter_id": "ABC123"}},{"id": 2,"full_name": "Jerry Dickinson"}]`
|
||||
|
||||
@ -238,7 +241,10 @@ func TestFilter2(t *testing.T) {
|
||||
value := `[{"id":1,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":100,"amount_refunded":0,"date":"01/01/2019","application":null,"billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}}, {"id":2,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":150,"amount_refunded":0,"date":"02/18/2019","billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}},{"id":3,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":150,"amount_refunded":50,"date":"03/21/2019","billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}}]`
|
||||
|
||||
var b bytes.Buffer
|
||||
Filter(&b, []byte(value), []string{"id"})
|
||||
err := Filter(&b, []byte(value), []string{"id"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
expected := `[{"id":1},{"id":2},{"id":3}]`
|
||||
|
||||
@ -253,7 +259,7 @@ func TestStrip(t *testing.T) {
|
||||
|
||||
expected := []byte(`[{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]`)
|
||||
|
||||
if bytes.Equal(value1, expected) == false {
|
||||
if !bytes.Equal(value1, expected) {
|
||||
t.Log(value1)
|
||||
t.Error("[Valid path] Does not match expected json")
|
||||
}
|
||||
@ -261,7 +267,7 @@ func TestStrip(t *testing.T) {
|
||||
path2 := [][]byte{[]byte("boo"), []byte("hoo")}
|
||||
value2 := Strip([]byte(input3), path2)
|
||||
|
||||
if bytes.Equal(value2, []byte(input3)) == false {
|
||||
if !bytes.Equal(value2, []byte(input3)) {
|
||||
t.Log(value2)
|
||||
t.Error("[Invalid path] Does not match expected json")
|
||||
}
|
||||
|
@ -16,8 +16,12 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
||||
tmap := make(map[uint64]int, len(from))
|
||||
|
||||
for i, f := range from {
|
||||
h.Write(f.Key)
|
||||
h.Write(f.Value)
|
||||
if _, err := h.Write(f.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := h.Write(f.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmap[h.Sum64()] = i
|
||||
h.Reset()
|
||||
@ -50,7 +54,9 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
||||
|
||||
case state == expectKeyClose && b[i] == '"':
|
||||
state = expectColon
|
||||
h.Write(b[(s + 1):i])
|
||||
if _, err := h.Write(b[(s + 1):i]); err != nil {
|
||||
return err
|
||||
}
|
||||
we = s
|
||||
|
||||
case state == expectColon && b[i] == ':':
|
||||
@ -106,7 +112,9 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
|
||||
if e != 0 {
|
||||
e++
|
||||
|
||||
h.Write(b[s:e])
|
||||
if _, err := h.Write(b[s:e]); err != nil {
|
||||
return err
|
||||
}
|
||||
n, ok := tmap[h.Sum64()]
|
||||
h.Reset()
|
||||
|
||||
|
@ -2,7 +2,6 @@ package jsn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
@ -333,15 +332,6 @@ func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
func s2b(s string) []byte {
|
||||
strh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
var sh reflect.SliceHeader
|
||||
sh.Data = strh.Data
|
||||
sh.Len = strh.Len
|
||||
sh.Cap = strh.Len
|
||||
return *(*[]byte)(unsafe.Pointer(&sh))
|
||||
}
|
||||
|
||||
const maxStartEndStringLen = 80
|
||||
|
||||
func startEndString(s string) string {
|
||||
|
@ -244,7 +244,6 @@ func (m *Migrator) AppendMigration(name, upSQL, downSQL string) {
|
||||
UpSQL: upSQL,
|
||||
DownSQL: downSQL,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate runs pending migrations
|
||||
@ -315,7 +314,7 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer tx.Rollback(ctx) //nolint: errcheck
|
||||
|
||||
// Fire on start callback
|
||||
if m.OnStart != nil {
|
||||
@ -332,7 +331,9 @@ func (m *Migrator) MigrateTo(targetVersion int32) (err error) {
|
||||
}
|
||||
|
||||
// Reset all database connection settings. Important to do before updating version as search_path may have been changed.
|
||||
tx.Exec(ctx, "reset all")
|
||||
if _, err := tx.Exec(ctx, "reset all"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add one to the version
|
||||
_, err = tx.Exec(ctx, "update "+m.versionTable+" set version=$1", sequence)
|
||||
|
@ -1,3 +1,4 @@
|
||||
//nolint:errcheck
|
||||
package psql
|
||||
|
||||
import (
|
||||
@ -19,7 +20,7 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables
|
||||
c := &compilerContext{w, qc.Selects, co}
|
||||
root := &qc.Selects[0]
|
||||
|
||||
ti, err := c.schema.GetTable(root.Table)
|
||||
ti, err := c.schema.GetTable(root.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -27,8 +27,11 @@ func TestMain(m *testing.M) {
|
||||
"token",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
qcompile.AddRole("user", "product", qcode.TRConfig{
|
||||
err = qcompile.AddRole("user", "product", qcode.TRConfig{
|
||||
Query: qcode.QueryConfig{
|
||||
Columns: []string{"id", "name", "price", "users", "customers"},
|
||||
Filters: []string{
|
||||
@ -54,27 +57,39 @@ func TestMain(m *testing.M) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
qcompile.AddRole("anon", "product", qcode.TRConfig{
|
||||
err = qcompile.AddRole("anon", "product", qcode.TRConfig{
|
||||
Query: qcode.QueryConfig{
|
||||
Columns: []string{"id", "name"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
qcompile.AddRole("anon1", "product", qcode.TRConfig{
|
||||
err = qcompile.AddRole("anon1", "product", qcode.TRConfig{
|
||||
Query: qcode.QueryConfig{
|
||||
Columns: []string{"id", "name", "price"},
|
||||
DisableFunctions: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
qcompile.AddRole("user", "users", qcode.TRConfig{
|
||||
err = qcompile.AddRole("user", "users", qcode.TRConfig{
|
||||
Query: qcode.QueryConfig{
|
||||
Columns: []string{"id", "full_name", "avatar", "email", "products"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
qcompile.AddRole("bad_dude", "users", qcode.TRConfig{
|
||||
err = qcompile.AddRole("bad_dude", "users", qcode.TRConfig{
|
||||
Query: qcode.QueryConfig{
|
||||
Filters: []string{"false"},
|
||||
DisableFunctions: true,
|
||||
@ -86,8 +101,11 @@ func TestMain(m *testing.M) {
|
||||
Filters: []string{"false"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
qcompile.AddRole("user", "mes", qcode.TRConfig{
|
||||
err = qcompile.AddRole("user", "mes", qcode.TRConfig{
|
||||
Query: qcode.QueryConfig{
|
||||
Columns: []string{"id", "full_name", "avatar"},
|
||||
Filters: []string{
|
||||
@ -95,8 +113,11 @@ func TestMain(m *testing.M) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
qcompile.AddRole("user", "customers", qcode.TRConfig{
|
||||
err = qcompile.AddRole("user", "customers", qcode.TRConfig{
|
||||
Query: qcode.QueryConfig{
|
||||
Columns: []string{"id", "email", "full_name", "products"},
|
||||
},
|
||||
@ -167,7 +188,9 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
for i, t := range tables {
|
||||
schema.updateSchema(t, columns[i], aliases)
|
||||
if err := schema.updateSchema(t, columns[i], aliases); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
vars := NewVariables(map[string]string{
|
||||
|
141
psql/query.go
141
psql/query.go
@ -1,3 +1,4 @@
|
||||
//nolint:errcheck
|
||||
package psql
|
||||
|
||||
import (
|
||||
@ -6,10 +7,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/util"
|
||||
)
|
||||
@ -137,7 +136,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
io.WriteString(c.w, `(`)
|
||||
}
|
||||
|
||||
ti, err := c.schema.GetTable(sel.Table)
|
||||
ti, err := c.schema.GetTable(sel.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -167,7 +166,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
|
||||
} else {
|
||||
sel := &c.s[(id - closeBlock)]
|
||||
|
||||
ti, err := c.schema.GetTable(sel.Table)
|
||||
ti, err := c.schema.GetTable(sel.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -213,7 +212,7 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
|
||||
for _, id := range sel.Children {
|
||||
child := &c.s[id]
|
||||
|
||||
rel, err := c.schema.GetRel(child.Table, ti.Name)
|
||||
rel, err := c.schema.GetRel(child.Name, ti.Name)
|
||||
if err != nil {
|
||||
skipped |= (1 << uint(id))
|
||||
continue
|
||||
@ -249,8 +248,8 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
||||
hasOrder := len(sel.OrderBy) != 0
|
||||
|
||||
// SELECT
|
||||
if ti.Singular == false {
|
||||
//fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Table)
|
||||
if !ti.Singular {
|
||||
//fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Name)
|
||||
io.WriteString(c.w, `SELECT coalesce(json_agg("`)
|
||||
io.WriteString(c.w, "json_")
|
||||
int2string(c.w, sel.ID)
|
||||
@ -263,7 +262,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.Name)
|
||||
io.WriteString(c.w, `), '[]')`)
|
||||
aliasWithID(c.w, "json", sel.ID)
|
||||
io.WriteString(c.w, ` FROM (`)
|
||||
@ -297,7 +296,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "json_row", sel.ID)
|
||||
|
||||
//fmt.Fprintf(w, `)) AS "%s"`, c.sel.Table)
|
||||
//fmt.Fprintf(w, `)) AS "%s"`, c.sel.Name)
|
||||
io.WriteString(c.w, `))`)
|
||||
aliasWithID(c.w, "json", sel.ID)
|
||||
// END-ROW-TO-JSON
|
||||
@ -351,7 +350,7 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo)
|
||||
io.WriteString(c.w, `') :: integer`)
|
||||
}
|
||||
|
||||
if ti.Singular == false {
|
||||
if !ti.Singular {
|
||||
//fmt.Fprintf(w, `) AS "json_agg_%d"`, c.sel.ID)
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "json_agg", sel.ID)
|
||||
@ -366,16 +365,16 @@ func (c *compilerContext) renderLateralJoin(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.Name, c.sel.ID)
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithIDSuffix(c.w, sel.Table, sel.ID, "_join")
|
||||
aliasWithIDSuffix(c.w, sel.Name, sel.ID, "_join")
|
||||
io.WriteString(c.w, ` ON ('true')`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderJoin(sel *qcode.Select, ti *DBTableInfo) error {
|
||||
parent := &c.s[sel.ParentID]
|
||||
return c.renderJoinByName(ti.Name, parent.Table, parent.ID)
|
||||
return c.renderJoinByName(ti.Name, parent.Name, parent.ID)
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderJoinByName(table, parent string, id int32) error {
|
||||
@ -396,7 +395,7 @@ func (c *compilerContext) renderJoinByName(table, parent string, id int32) error
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, ` LEFT OUTER JOIN "%s" ON (("%s"."%s") = ("%s_%d"."%s"))`,
|
||||
//rel.Through, rel.Through, rel.ColT, c.parent.Table, c.parent.ID, rel.Col1)
|
||||
//rel.Through, rel.Through, rel.ColT, c.parent.Name, c.parent.ID, rel.Col1)
|
||||
io.WriteString(c.w, ` LEFT OUTER JOIN "`)
|
||||
io.WriteString(c.w, rel.Through)
|
||||
io.WriteString(c.w, `" ON ((`)
|
||||
@ -417,7 +416,7 @@ func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) {
|
||||
for _, col := range sel.Cols {
|
||||
n := funcPrefixLen(col.Name)
|
||||
if n != 0 {
|
||||
if sel.Functions == false {
|
||||
if !sel.Functions {
|
||||
continue
|
||||
}
|
||||
if len(sel.Allowed) != 0 {
|
||||
@ -437,7 +436,7 @@ func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) {
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
|
||||
//c.sel.Table, c.sel.ID, col.Name, col.FieldName)
|
||||
//c.sel.Name, c.sel.ID, col.Name, col.FieldName)
|
||||
colWithTableIDAlias(c.w, ti.Name, sel.ID, col.Name, col.FieldName)
|
||||
i++
|
||||
}
|
||||
@ -449,7 +448,7 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableI
|
||||
for _, id := range sel.Children {
|
||||
child := &c.s[id]
|
||||
|
||||
rel, err := c.schema.GetRel(child.Table, sel.Table)
|
||||
rel, err := c.schema.GetRel(child.Name, sel.Name)
|
||||
if err != nil || rel.Type != RelRemote {
|
||||
continue
|
||||
}
|
||||
@ -457,7 +456,7 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableI
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
|
||||
//c.sel.Table, c.sel.ID, rel.Col1, rel.Col2)
|
||||
//c.sel.Name, c.sel.ID, rel.Col1, rel.Col2)
|
||||
colWithTableID(c.w, ti.Name, sel.ID, rel.Col1)
|
||||
alias(c.w, rel.Col2)
|
||||
i++
|
||||
@ -479,10 +478,10 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo
|
||||
childSel := &c.s[id]
|
||||
|
||||
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
|
||||
//s.Table, s.ID, s.Table, s.FieldName)
|
||||
//s.Name, s.ID, s.Name, s.FieldName)
|
||||
//if cti.Singular {
|
||||
io.WriteString(c.w, `"`)
|
||||
io.WriteString(c.w, childSel.Table)
|
||||
io.WriteString(c.w, childSel.Name)
|
||||
io.WriteString(c.w, `_`)
|
||||
int2string(c.w, childSel.ID)
|
||||
io.WriteString(c.w, `_join"."json_`)
|
||||
@ -500,7 +499,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
var groupBy []int
|
||||
|
||||
isRoot := sel.ParentID == -1
|
||||
isFil := sel.Where != nil
|
||||
isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop)
|
||||
isSearch := sel.Args["search"] != nil
|
||||
isAgg := false
|
||||
|
||||
@ -523,7 +522,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
//fmt.Fprintf(w, `ts_rank("%s"."%s", to_tsquery('%s')) AS %s`,
|
||||
//c.sel.Table, cn, arg.Val, col.Name)
|
||||
//c.sel.Name, cn, arg.Val, col.Name)
|
||||
io.WriteString(c.w, `ts_rank(`)
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
io.WriteString(c.w, `, to_tsquery('`)
|
||||
@ -540,7 +539,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
//fmt.Fprintf(w, `ts_headline("%s"."%s", to_tsquery('%s')) AS %s`,
|
||||
//c.sel.Table, cn, arg.Val, col.Name)
|
||||
//c.sel.Name, cn, arg.Val, col.Name)
|
||||
io.WriteString(c.w, `ts_headlinek(`)
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
io.WriteString(c.w, `, to_tsquery('`)
|
||||
@ -576,7 +575,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
fn := cn[0 : pl-1]
|
||||
isAgg = true
|
||||
|
||||
//fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Table, cn, col.Name)
|
||||
//fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Name, cn, col.Name)
|
||||
io.WriteString(c.w, fn)
|
||||
io.WriteString(c.w, `(`)
|
||||
colWithTable(c.w, ti.Name, cn1)
|
||||
@ -588,7 +587,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
}
|
||||
} else {
|
||||
groupBy = append(groupBy, n)
|
||||
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Table, cn)
|
||||
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, cn)
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
@ -610,18 +609,18 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
|
||||
io.WriteString(c.w, ` FROM `)
|
||||
|
||||
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Table)
|
||||
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name)
|
||||
io.WriteString(c.w, `"`)
|
||||
io.WriteString(c.w, ti.Name)
|
||||
io.WriteString(c.w, `"`)
|
||||
|
||||
// if tn, ok := c.tmap[sel.Table]; ok {
|
||||
// //fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Table)
|
||||
// tableWithAlias(c.w, ti.Name, sel.Table)
|
||||
// if tn, ok := c.tmap[sel.Name]; ok {
|
||||
// //fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Name)
|
||||
// tableWithAlias(c.w, ti.Name, sel.Name)
|
||||
// } else {
|
||||
// //fmt.Fprintf(w, ` FROM "%s"`, c.sel.Table)
|
||||
// //fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name)
|
||||
// io.WriteString(c.w, `"`)
|
||||
// io.WriteString(c.w, sel.Table)
|
||||
// io.WriteString(c.w, sel.Name)
|
||||
// io.WriteString(c.w, `"`)
|
||||
// }
|
||||
|
||||
@ -661,7 +660,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Table, c.sel.Cols[id].Name)
|
||||
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, c.sel.Cols[id].Name)
|
||||
colWithTable(c.w, ti.Name, sel.Cols[id].Name)
|
||||
}
|
||||
}
|
||||
@ -691,7 +690,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
||||
io.WriteString(c.w, `') :: integer`)
|
||||
}
|
||||
|
||||
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Table, c.sel.ID)
|
||||
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID)
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, ti.Name, sel.ID)
|
||||
return nil
|
||||
@ -708,17 +707,17 @@ func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInf
|
||||
|
||||
col := sel.OrderBy[i].Col
|
||||
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`,
|
||||
//c.sel.Table, c.sel.ID, c,
|
||||
//c.sel.Table, c.sel.ID, c)
|
||||
//c.sel.Name, c.sel.ID, c,
|
||||
//c.sel.Name, c.sel.ID, c)
|
||||
colWithTableID(c.w, ti.Name, sel.ID, col)
|
||||
io.WriteString(c.w, ` AS `)
|
||||
tableIDColSuffix(c.w, sel.Table, sel.ID, col, "_ob")
|
||||
tableIDColSuffix(c.w, sel.Name, sel.ID, col, "_ob")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
||||
parent := c.s[sel.ParentID]
|
||||
return c.renderRelationshipByName(ti.Name, parent.Table, parent.ID)
|
||||
return c.renderRelationshipByName(ti.Name, parent.Name, parent.ID)
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderRelationshipByName(table, parent string, id int32) error {
|
||||
@ -730,7 +729,7 @@ func (c *compilerContext) renderRelationshipByName(table, parent string, id int3
|
||||
switch rel.Type {
|
||||
case RelBelongTo:
|
||||
//fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`,
|
||||
//c.sel.Table, rel.Col1, c.parent.Table, c.parent.ID, rel.Col2)
|
||||
//c.sel.Name, rel.Col1, c.parent.Name, c.parent.ID, rel.Col2)
|
||||
io.WriteString(c.w, `((`)
|
||||
colWithTable(c.w, table, rel.Col1)
|
||||
io.WriteString(c.w, `) = (`)
|
||||
@ -743,7 +742,7 @@ func (c *compilerContext) renderRelationshipByName(table, parent string, id int3
|
||||
|
||||
case RelOneToMany:
|
||||
//fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`,
|
||||
//c.sel.Table, rel.Col1, c.parent.Table, c.parent.ID, rel.Col2)
|
||||
//c.sel.Name, rel.Col1, c.parent.Name, c.parent.ID, rel.Col2)
|
||||
io.WriteString(c.w, `((`)
|
||||
colWithTable(c.w, table, rel.Col1)
|
||||
io.WriteString(c.w, `) = (`)
|
||||
@ -757,7 +756,7 @@ func (c *compilerContext) renderRelationshipByName(table, parent string, id int3
|
||||
case RelOneToManyThrough:
|
||||
// This requires the through table to be joined onto this select
|
||||
//fmt.Fprintf(w, `(("%s"."%s") = ("%s"."%s"))`,
|
||||
//c.sel.Table, rel.Col1, rel.Through, rel.Col2)
|
||||
//c.sel.Name, rel.Col1, rel.Through, rel.Col2)
|
||||
io.WriteString(c.w, `((`)
|
||||
colWithTable(c.w, table, rel.Col1)
|
||||
io.WriteString(c.w, `) = (`)
|
||||
@ -826,7 +825,7 @@ func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error
|
||||
}
|
||||
|
||||
} else {
|
||||
//fmt.Fprintf(w, `(("%s"."%s") `, c.sel.Table, val.Col)
|
||||
//fmt.Fprintf(w, `(("%s"."%s") `, c.sel.Name, val.Col)
|
||||
if err := c.renderOp(val, sel, ti); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -880,6 +879,10 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, sel *qcode.Select, ti *DBTable
|
||||
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)
|
||||
@ -952,7 +955,7 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, sel *qcode.Select, ti *DBTable
|
||||
if len(ti.TSVCol) == 0 {
|
||||
return fmt.Errorf("no tsv column defined for %s", ti.Name)
|
||||
}
|
||||
if col, ok = ti.Columns[ti.TSVCol]; !ok {
|
||||
if _, ok = ti.Columns[ti.TSVCol]; !ok {
|
||||
return fmt.Errorf("no tsv column '%s' found ", ti.TSVCol)
|
||||
}
|
||||
//fmt.Fprintf(w, `(("%s") @@ to_tsquery('%s'))`, c.ti.TSVCol, val.Val)
|
||||
@ -987,27 +990,27 @@ func (c *compilerContext) renderOrderBy(sel *qcode.Select, ti *DBTableInfo) erro
|
||||
|
||||
switch ob.Order {
|
||||
case qcode.OrderAsc:
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Table, sel.ID, ob.Col)
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Name, sel.ID, ob.Col)
|
||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
||||
io.WriteString(c.w, ` ASC`)
|
||||
case qcode.OrderDesc:
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" DESC`, sel.Table, sel.ID, ob.Col)
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" DESC`, sel.Name, sel.ID, ob.Col)
|
||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
||||
io.WriteString(c.w, ` DESC`)
|
||||
case qcode.OrderAscNullsFirst:
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC NULLS FIRST`, sel.Table, sel.ID, ob.Col)
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC NULLS FIRST`, sel.Name, sel.ID, ob.Col)
|
||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
||||
io.WriteString(c.w, ` ASC NULLS FIRST`)
|
||||
case qcode.OrderDescNullsFirst:
|
||||
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS FIRST`, sel.Table, sel.ID, ob.Col)
|
||||
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS FIRST`, sel.Name, sel.ID, ob.Col)
|
||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
||||
io.WriteString(c.w, ` DESC NULLLS FIRST`)
|
||||
case qcode.OrderAscNullsLast:
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s ASC NULLS LAST`, sel.Table, sel.ID, ob.Col)
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s ASC NULLS LAST`, sel.Name, sel.ID, ob.Col)
|
||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
||||
io.WriteString(c.w, ` ASC NULLS LAST`)
|
||||
case qcode.OrderDescNullsLast:
|
||||
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Table, sel.ID, ob.Col)
|
||||
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Name, sel.ID, ob.Col)
|
||||
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
||||
io.WriteString(c.w, ` DESC NULLS LAST`)
|
||||
default:
|
||||
@ -1023,7 +1026,7 @@ func (c *compilerContext) renderDistinctOn(sel *qcode.Select, ti *DBTableInfo) {
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s"`, c.sel.Table, c.sel.ID, c.sel.DistinctOn[i])
|
||||
//fmt.Fprintf(w, `"%s_%d.ob.%s"`, c.sel.Name, c.sel.ID, c.sel.DistinctOn[i])
|
||||
tableIDColSuffix(c.w, ti.Name, sel.ID, sel.DistinctOn[i], "_ob")
|
||||
}
|
||||
io.WriteString(c.w, `) `)
|
||||
@ -1135,22 +1138,6 @@ func aliasWithIDSuffix(w io.Writer, alias string, id int32, suffix string) {
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func colWithAlias(w io.Writer, col, alias string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, col)
|
||||
io.WriteString(w, `" AS "`)
|
||||
io.WriteString(w, alias)
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func tableWithAlias(w io.Writer, table, alias string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, table)
|
||||
io.WriteString(w, `" AS "`)
|
||||
io.WriteString(w, alias)
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func colWithTable(w io.Writer, table, col string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, table)
|
||||
@ -1181,20 +1168,6 @@ func colWithTableIDAlias(w io.Writer, table string, id int32, col, alias string)
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func colWithTableIDSuffixAlias(w io.Writer, table string, id int32,
|
||||
suffix, col, alias string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, table)
|
||||
io.WriteString(w, `_`)
|
||||
int2string(w, id)
|
||||
io.WriteString(w, suffix)
|
||||
io.WriteString(w, `"."`)
|
||||
io.WriteString(w, col)
|
||||
io.WriteString(w, `" AS "`)
|
||||
io.WriteString(w, alias)
|
||||
io.WriteString(w, `"`)
|
||||
}
|
||||
|
||||
func tableIDColSuffix(w io.Writer, table string, id int32, col, suffix string) {
|
||||
io.WriteString(w, `"`)
|
||||
io.WriteString(w, table)
|
||||
@ -1219,7 +1192,7 @@ func int2string(w io.Writer, val int32) {
|
||||
for val2 > 0 {
|
||||
temp *= 10
|
||||
temp += val2 % 10
|
||||
val2 = int32(math.Floor(float64(val2 / 10)))
|
||||
val2 = int32(float64(val2 / 10))
|
||||
}
|
||||
|
||||
val3 := temp
|
||||
@ -1229,11 +1202,3 @@ func int2string(w io.Writer, val int32) {
|
||||
w.Write([]byte{charset[d]})
|
||||
}
|
||||
}
|
||||
|
||||
func relID(h *xxhash.Digest, child, parent string) uint64 {
|
||||
h.WriteString(child)
|
||||
h.WriteString(parent)
|
||||
v := h.Sum64()
|
||||
h.Reset()
|
||||
return v
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ func oneToMany(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) 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"`
|
||||
sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND (("products"."price") > 0) AND (("products"."price") < 8)) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
@ -240,7 +240,7 @@ func manyToManyReverse(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('customers', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id"))) 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"`
|
||||
sql := `SELECT json_object_agg('customers', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND (("products"."price") > 0) AND (("products"."price") < 8)) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql, nil, "user")
|
||||
if err != nil {
|
||||
|
@ -199,7 +199,9 @@ func NewDBSchema(db *pgxpool.Pool, aliases map[string][]string) (*DBSchema, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
schema.updateSchema(t, cols, aliases)
|
||||
if err := schema.updateSchema(t, cols, aliases); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
@ -208,7 +210,7 @@ func NewDBSchema(db *pgxpool.Pool, aliases map[string][]string) (*DBSchema, erro
|
||||
func (s *DBSchema) updateSchema(
|
||||
t *DBTable,
|
||||
cols []*DBColumn,
|
||||
aliases map[string][]string) {
|
||||
aliases map[string][]string) error {
|
||||
|
||||
// Foreign key columns in current table
|
||||
colByID := make(map[int16]*DBColumn)
|
||||
@ -281,12 +283,16 @@ func (s *DBSchema) updateSchema(
|
||||
// Belongs-to relation between current table and the
|
||||
// table in the foreign key
|
||||
rel1 := &DBRel{RelBelongTo, "", "", c.Name, fc.Name}
|
||||
s.SetRel(ct, ft, rel1)
|
||||
if err := s.SetRel(ct, ft, rel1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// One-to-many relation between the foreign key table and the
|
||||
// the current table
|
||||
rel2 := &DBRel{RelOneToMany, "", "", fc.Name, c.Name}
|
||||
s.SetRel(ft, ct, rel2)
|
||||
if err := s.SetRel(ft, ct, rel2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jcols = append(jcols, c)
|
||||
}
|
||||
@ -301,42 +307,54 @@ func (s *DBSchema) updateSchema(
|
||||
if len(jcols) > 1 {
|
||||
for i := range jcols {
|
||||
for n := range jcols {
|
||||
if n != i {
|
||||
s.updateSchemaOTMT(ct, jcols[i], jcols[n], colByID)
|
||||
if n == i {
|
||||
continue
|
||||
}
|
||||
err := s.updateSchemaOTMT(ct, jcols[i], jcols[n], colByID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DBSchema) updateSchemaOTMT(
|
||||
ct string,
|
||||
col1, col2 *DBColumn,
|
||||
colByID map[int16]*DBColumn) {
|
||||
colByID map[int16]*DBColumn) error {
|
||||
|
||||
t1 := strings.ToLower(col1.FKeyTable)
|
||||
t2 := strings.ToLower(col2.FKeyTable)
|
||||
|
||||
fc1, ok := colByID[col1.FKeyColID[0]]
|
||||
if !ok {
|
||||
return
|
||||
return fmt.Errorf("expected column id '%d' not found", col1.FKeyColID[0])
|
||||
}
|
||||
fc2, ok := colByID[col2.FKeyColID[0]]
|
||||
if !ok {
|
||||
return
|
||||
return fmt.Errorf("expected column id '%d' not found", col2.FKeyColID[0])
|
||||
}
|
||||
|
||||
// One-to-many-through relation between 1nd foreign key table and the
|
||||
// 2nd foreign key table
|
||||
//rel1 := &DBRel{RelOneToManyThrough, ct, fc1.Name, col1.Name}
|
||||
rel1 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name, col1.Name}
|
||||
s.SetRel(t1, t2, rel1)
|
||||
if err := s.SetRel(t1, t2, rel1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// One-to-many-through relation between 2nd foreign key table and the
|
||||
// 1nd foreign key table
|
||||
//rel2 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name}
|
||||
rel2 := &DBRel{RelOneToManyThrough, ct, col1.Name, fc1.Name, col2.Name}
|
||||
s.SetRel(t2, t1, rel2)
|
||||
if err := s.SetRel(t2, t1, rel2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {
|
||||
|
@ -116,7 +116,7 @@ func listToMap(list []string) map[string]struct{} {
|
||||
|
||||
func mapToList(m map[string]string) []string {
|
||||
list := []string{}
|
||||
for k, _ := range m {
|
||||
for k := range m {
|
||||
list = append(list, strings.ToLower(k))
|
||||
}
|
||||
sort.Strings(list)
|
||||
|
@ -4,6 +4,8 @@ package qcode
|
||||
|
||||
// FuzzerEntrypoint for Fuzzbuzz
|
||||
func Fuzz(data []byte) int {
|
||||
GetQType(string(data))
|
||||
|
||||
qcompile, _ := NewCompiler(Config{})
|
||||
_, err := qcompile.Compile(data, "user")
|
||||
if err != nil {
|
||||
|
@ -461,7 +461,7 @@ func (i *item) String() string {
|
||||
case itemStringVal:
|
||||
v = "string"
|
||||
}
|
||||
return fmt.Sprintf("%s", v)
|
||||
return v
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -85,7 +85,6 @@ type Parser struct {
|
||||
input []byte // the string being scanned
|
||||
pos int
|
||||
items []item
|
||||
depth int
|
||||
err error
|
||||
}
|
||||
|
||||
@ -185,15 +184,6 @@ func (p *Parser) ignore() {
|
||||
p.pos = n
|
||||
}
|
||||
|
||||
func (p *Parser) current() item {
|
||||
return p.items[p.pos]
|
||||
}
|
||||
|
||||
func (p *Parser) eof() bool {
|
||||
n := p.pos + 1
|
||||
return p.items[n].typ == itemEOF
|
||||
}
|
||||
|
||||
func (p *Parser) peek(types ...itemType) bool {
|
||||
n := p.pos + 1
|
||||
if p.items[n].typ == itemEOF {
|
||||
@ -250,7 +240,7 @@ func (p *Parser) parseOp() (*Operation, error) {
|
||||
p.ignore()
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
if p.peek(itemName) == false {
|
||||
if !p.peek(itemName) {
|
||||
break
|
||||
}
|
||||
|
||||
@ -275,7 +265,7 @@ func (p *Parser) parseQueryOp() (*Operation, error) {
|
||||
var err error
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
if p.peek(itemName) == false {
|
||||
if !p.peek(itemName) {
|
||||
break
|
||||
}
|
||||
|
||||
@ -306,7 +296,7 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.peek(itemName) == false {
|
||||
if !p.peek(itemName) {
|
||||
return nil, errors.New("expecting an alias or field name")
|
||||
}
|
||||
|
||||
@ -374,13 +364,13 @@ func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
|
||||
p.ignore()
|
||||
break
|
||||
}
|
||||
if p.peek(itemName) == false {
|
||||
if !p.peek(itemName) {
|
||||
return nil, errors.New("expecting an argument name")
|
||||
}
|
||||
args = append(args, Arg{Name: p.val(p.next())})
|
||||
arg := &args[(len(args) - 1)]
|
||||
|
||||
if p.peek(itemColon) == false {
|
||||
if !p.peek(itemColon) {
|
||||
return nil, errors.New("missing ':' after argument name")
|
||||
}
|
||||
p.ignore()
|
||||
@ -441,12 +431,12 @@ func (p *Parser) parseObj() (*Node, error) {
|
||||
break
|
||||
}
|
||||
|
||||
if p.peek(itemName) == false {
|
||||
if !p.peek(itemName) {
|
||||
return nil, errors.New("expecting an argument name")
|
||||
}
|
||||
nodeName := p.val(p.next())
|
||||
|
||||
if p.peek(itemColon) == false {
|
||||
if !p.peek(itemColon) {
|
||||
return nil, errors.New("missing ':' after Field argument name")
|
||||
}
|
||||
p.ignore()
|
||||
|
@ -7,13 +7,16 @@ import (
|
||||
|
||||
func TestCompile1(t *testing.T) {
|
||||
qc, _ := NewCompiler(Config{})
|
||||
qc.AddRole("user", "product", TRConfig{
|
||||
err := qc.AddRole("user", "product", TRConfig{
|
||||
Query: QueryConfig{
|
||||
Columns: []string{"id", "Name"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err := qc.Compile([]byte(`
|
||||
_, err = qc.Compile([]byte(`
|
||||
{ product(id: 15) {
|
||||
id
|
||||
name
|
||||
@ -26,13 +29,16 @@ func TestCompile1(t *testing.T) {
|
||||
|
||||
func TestCompile2(t *testing.T) {
|
||||
qc, _ := NewCompiler(Config{})
|
||||
qc.AddRole("user", "product", TRConfig{
|
||||
err := qc.AddRole("user", "product", TRConfig{
|
||||
Query: QueryConfig{
|
||||
Columns: []string{"ID"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err := qc.Compile([]byte(`
|
||||
_, err = qc.Compile([]byte(`
|
||||
query { product(id: 15) {
|
||||
id
|
||||
name
|
||||
@ -45,13 +51,16 @@ func TestCompile2(t *testing.T) {
|
||||
|
||||
func TestCompile3(t *testing.T) {
|
||||
qc, _ := NewCompiler(Config{})
|
||||
qc.AddRole("user", "product", TRConfig{
|
||||
err := qc.AddRole("user", "product", TRConfig{
|
||||
Query: QueryConfig{
|
||||
Columns: []string{"ID"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err := qc.Compile([]byte(`
|
||||
_, err = qc.Compile([]byte(`
|
||||
mutation {
|
||||
product(id: 15, name: "Test") {
|
||||
id
|
||||
|
@ -20,6 +20,7 @@ const (
|
||||
|
||||
const (
|
||||
QTQuery QType = iota + 1
|
||||
QTMutation
|
||||
QTInsert
|
||||
QTUpdate
|
||||
QTDelete
|
||||
@ -38,7 +39,7 @@ type Select struct {
|
||||
ID int32
|
||||
ParentID int32
|
||||
Args map[string]*Node
|
||||
Table string
|
||||
Name string
|
||||
FieldName string
|
||||
Cols []Column
|
||||
Where *Exp
|
||||
@ -300,7 +301,7 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
||||
selects = append(selects, Select{
|
||||
ID: id,
|
||||
ParentID: parentID,
|
||||
Table: field.Name,
|
||||
Name: field.Name,
|
||||
Children: make([]int32, 0, 5),
|
||||
Allowed: trv.allowedColumns(action),
|
||||
Functions: true,
|
||||
@ -324,7 +325,7 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
||||
if len(field.Alias) != 0 {
|
||||
s.FieldName = field.Alias
|
||||
} else {
|
||||
s.FieldName = s.Table
|
||||
s.FieldName = s.Name
|
||||
}
|
||||
|
||||
err := com.compileArgs(qc, s, field.Args)
|
||||
@ -333,9 +334,10 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
||||
}
|
||||
|
||||
// Order is important addFilters must come after compileArgs
|
||||
com.addFilters(qc, s, role)
|
||||
|
||||
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)
|
||||
@ -381,7 +383,7 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
||||
func (com *Compiler) addFilters(qc *QCode, root *Select, role string) {
|
||||
var fil *Exp
|
||||
|
||||
if trv, ok := com.tr[role][root.Table]; ok {
|
||||
if trv, ok := com.tr[role][root.Name]; ok {
|
||||
fil = trv.filter(qc.Type)
|
||||
}
|
||||
|
||||
|
23
qcode/utils.go
Normal file
23
qcode/utils.go
Normal file
@ -0,0 +1,23 @@
|
||||
package qcode
|
||||
|
||||
func GetQType(gql string) QType {
|
||||
for i := range gql {
|
||||
b := gql[i]
|
||||
if b == '{' {
|
||||
return QTQuery
|
||||
}
|
||||
if al(b) {
|
||||
switch b {
|
||||
case 'm', 'M':
|
||||
return QTMutation
|
||||
case 'q', 'Q':
|
||||
return QTQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func al(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||
}
|
124
serv/allow.go
124
serv/allow.go
@ -18,6 +18,8 @@ const (
|
||||
)
|
||||
|
||||
type allowItem struct {
|
||||
name string
|
||||
hash string
|
||||
uri string
|
||||
gql string
|
||||
vars json.RawMessage
|
||||
@ -46,7 +48,7 @@ func initAllowList(cpath string) {
|
||||
if _, err := os.Stat(fp); err == nil {
|
||||
_allowList.filepath = fp
|
||||
} 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 {
|
||||
_allowList.filepath = fp
|
||||
} 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 {
|
||||
_allowList.filepath = fp
|
||||
} else if !os.IsNotExist(err) {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
if len(_allowList.filepath) == 0 {
|
||||
if conf.Production {
|
||||
logger.Fatal().Msg("allow.list not found")
|
||||
errlog.Fatal().Msg("allow.list not found")
|
||||
}
|
||||
|
||||
if len(cpath) == 0 {
|
||||
@ -94,7 +96,7 @@ func initAllowList(cpath string) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -119,11 +121,39 @@ func (al *allowList) add(req *gqlReq) {
|
||||
}
|
||||
}
|
||||
|
||||
func (al *allowList) load() {
|
||||
if al.active == false {
|
||||
return
|
||||
func (al *allowList) upsert(query, vars []byte, uri string) {
|
||||
q := string(query)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@ -172,22 +202,7 @@ func (al *allowList) load() {
|
||||
|
||||
if c == 0 {
|
||||
if ty == AL_QUERY {
|
||||
q := string(b[s:(e + 1)])
|
||||
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
|
||||
}
|
||||
|
||||
al.upsert(b[s:(e+1)], varBytes, uri)
|
||||
varBytes = nil
|
||||
|
||||
} else if ty == AL_VARS {
|
||||
@ -205,19 +220,35 @@ func (al *allowList) load() {
|
||||
}
|
||||
|
||||
func (al *allowList) save(item *allowItem) {
|
||||
if al.active == false {
|
||||
return
|
||||
var err error
|
||||
|
||||
item.hash = gqlHash(item.gql, item.vars, "")
|
||||
item.name = gqlName(item.gql)
|
||||
|
||||
if len(item.name) == 0 {
|
||||
key := item.hash
|
||||
|
||||
if _, ok := al.index[key]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
al.list = append(al.list, item)
|
||||
al.index[key] = len(al.list) - 1
|
||||
|
||||
} else {
|
||||
key := item.name
|
||||
|
||||
if i, ok := al.index[key]; ok {
|
||||
if al.list[i].hash == item.hash {
|
||||
return
|
||||
}
|
||||
al.list[i] = item
|
||||
} else {
|
||||
al.list = append(al.list, item)
|
||||
al.index[key] = len(al.list) - 1
|
||||
}
|
||||
}
|
||||
|
||||
key := gqlHash(item.gql, item.vars, "")
|
||||
|
||||
if _, ok := al.index[key]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
al.list = append(al.list, item)
|
||||
al.index[key] = len(al.list) - 1
|
||||
|
||||
f, err := os.Create(al.filepath)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msgf("Failed to write allow list: %s", al.filepath)
|
||||
@ -242,22 +273,35 @@ func (al *allowList) save(item *allowItem) {
|
||||
k := keys[i]
|
||||
v := urlMap[k]
|
||||
|
||||
f.WriteString(fmt.Sprintf("# %s\n\n", k))
|
||||
if _, err := f.WriteString(fmt.Sprintf("# %s\n\n", k)); err != nil {
|
||||
logger.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
for i := range v {
|
||||
if len(v[i].vars) != 0 && bytes.Equal(v[i].vars, []byte("{}")) == false {
|
||||
if len(v[i].vars) != 0 && !bytes.Equal(v[i].vars, []byte("{}")) {
|
||||
vj, err := json.MarshalIndent(v[i].vars, "", "\t")
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Failed to write allow list 'vars' to file")
|
||||
continue
|
||||
}
|
||||
f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
|
||||
|
||||
_, err = f.WriteString(fmt.Sprintf("variables %s\n\n", vj))
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if v[i].gql[0] == '{' {
|
||||
f.WriteString(fmt.Sprintf("query %s\n\n", v[i].gql))
|
||||
_, err = f.WriteString(fmt.Sprintf("query %s\n\n", v[i].gql))
|
||||
} else {
|
||||
f.WriteString(fmt.Sprintf("%s\n\n", v[i].gql))
|
||||
_, err = f.WriteString(fmt.Sprintf("%s\n\n", v[i].gql))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
serv/args.go
29
serv/args.go
@ -2,44 +2,46 @@ package serv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
)
|
||||
|
||||
func argMap(ctx *coreContext) func(w io.Writer, tag string) (int, error) {
|
||||
func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int, error) {
|
||||
return func(w io.Writer, tag string) (int, error) {
|
||||
switch tag {
|
||||
case "user_id_provider":
|
||||
if v := ctx.Value(userIDProviderKey); v != nil {
|
||||
return io.WriteString(w, v.(string))
|
||||
}
|
||||
return io.WriteString(w, "null")
|
||||
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 io.WriteString(w, "null")
|
||||
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 io.WriteString(w, "null")
|
||||
return 0, errors.New("query requires variable $user_role")
|
||||
}
|
||||
|
||||
fields := jsn.Get(ctx.req.Vars, [][]byte{[]byte(tag)})
|
||||
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
|
||||
if len(fields) == 0 {
|
||||
return 0, fmt.Errorf("variable '%s' not found", tag)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return w.Write(fields[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func argList(ctx *coreContext, args [][]byte) []interface{} {
|
||||
func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
||||
vars := make([]interface{}, len(args))
|
||||
|
||||
var fields map[string]interface{}
|
||||
@ -49,7 +51,7 @@ func argList(ctx *coreContext, args [][]byte) []interface{} {
|
||||
fields, _, err = jsn.Tree(ctx.req.Vars)
|
||||
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Failed to parse variables")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,24 +62,33 @@ func argList(ctx *coreContext, args [][]byte) []interface{} {
|
||||
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
|
||||
return vars, nil
|
||||
}
|
||||
|
@ -11,8 +11,7 @@ import (
|
||||
|
||||
const (
|
||||
authHeader = "Authorization"
|
||||
jwtBase int = iota
|
||||
jwtAuth0
|
||||
jwtAuth0 int = iota + 1
|
||||
)
|
||||
|
||||
func jwtHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
@ -35,7 +34,7 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
case len(publicKeyFile) != 0:
|
||||
kd, err := ioutil.ReadFile(publicKeyFile)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
switch conf.Auth.JWT.PubKeyType {
|
||||
@ -51,7 +50,7 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,11 +15,11 @@ import (
|
||||
func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
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 {
|
||||
logger.Fatal().Msg("no auth.rails.url defined")
|
||||
errlog.Fatal().Msg("no auth.rails.url defined")
|
||||
}
|
||||
|
||||
rp := &redis.Pool{
|
||||
@ -28,13 +28,13 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
Dial: func() (redis.Conn, error) {
|
||||
c, err := redis.DialURL(conf.Auth.Rails.URL)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
pwd := conf.Auth.Rails.Password
|
||||
if len(pwd) != 0 {
|
||||
if _, err := c.Do("AUTH", pwd); err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
}
|
||||
return c, err
|
||||
@ -69,16 +69,16 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
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 {
|
||||
logger.Fatal().Msg("no auth.rails.url defined")
|
||||
errlog.Fatal().Msg("no auth.rails.url defined")
|
||||
}
|
||||
|
||||
rURL, err := url.Parse(conf.Auth.Rails.URL)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
mc := memcache.New(rURL.Host)
|
||||
@ -111,12 +111,12 @@ func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
func railsCookieHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
if len(cookie) == 0 {
|
||||
logger.Fatal().Msg("no auth.cookie defined")
|
||||
errlog.Fatal().Msg("no auth.cookie defined")
|
||||
}
|
||||
|
||||
ra, err := railsAuth(conf)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
89
serv/cmd.go
89
serv/cmd.go
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/dosco/super-graph/psql"
|
||||
@ -22,20 +23,30 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
logger *zerolog.Logger
|
||||
conf *config
|
||||
confPath string
|
||||
db *pgxpool.Pool
|
||||
qcompile *qcode.Compiler
|
||||
pcompile *psql.Compiler
|
||||
// These variables are set using -ldflags
|
||||
version string
|
||||
gitBranch string
|
||||
lastCommitSHA string
|
||||
lastCommitTime string
|
||||
)
|
||||
|
||||
var (
|
||||
logger zerolog.Logger // logger for everything but errors
|
||||
errlog zerolog.Logger // logger for errors includes line numbers
|
||||
conf *config // parsed config
|
||||
confPath string // path to the config file
|
||||
db *pgxpool.Pool // database connection pool
|
||||
schema *psql.DBSchema // database tables, columns and relationships
|
||||
qcompile *qcode.Compiler // qcode compiler
|
||||
pcompile *psql.Compiler // postgres sql compiler
|
||||
)
|
||||
|
||||
func Init() {
|
||||
logger = initLog()
|
||||
initLog()
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "super-graph",
|
||||
Short: "An instant high-performance GraphQL API. No code needed. https://supergraph.dev",
|
||||
Short: BuildDetails(),
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
@ -131,23 +142,24 @@ e.g. db:migrate -+1
|
||||
Run: cmdConfDump,
|
||||
})
|
||||
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Super Graph binary version information",
|
||||
Run: cmdVersion,
|
||||
})
|
||||
|
||||
rootCmd.Flags().StringVar(&confPath,
|
||||
"path", "./config", "path to config files")
|
||||
|
||||
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}
|
||||
logger := zerolog.New(out).
|
||||
With().
|
||||
Timestamp().
|
||||
Caller().
|
||||
Logger()
|
||||
|
||||
return &logger
|
||||
logger = zerolog.New(out).With().Timestamp().Logger()
|
||||
errlog = logger.With().Caller().Logger()
|
||||
}
|
||||
|
||||
func initConf() (*config, error) {
|
||||
@ -166,13 +178,16 @@ func initConf() (*config, error) {
|
||||
}
|
||||
|
||||
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,
|
||||
vi.GetString("inherits"))
|
||||
}
|
||||
|
||||
vi.SetConfigName(getConfigName())
|
||||
vi.MergeInConfig()
|
||||
|
||||
if err := vi.MergeInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c := &config{}
|
||||
@ -183,7 +198,7 @@ func initConf() (*config, error) {
|
||||
|
||||
logLevel, err := zerolog.ParseLevel(c.LogLevel)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("error setting log_level")
|
||||
errlog.Error().Err(err).Msg("error setting log_level")
|
||||
}
|
||||
zerolog.SetGlobalLevel(logLevel)
|
||||
|
||||
@ -218,7 +233,7 @@ func initDB(c *config, useDB bool) (*pgx.Conn, error) {
|
||||
config.LogLevel = pgx.LogLevelNone
|
||||
}
|
||||
|
||||
config.Logger = NewSQLLogger(*logger)
|
||||
config.Logger = NewSQLLogger(logger)
|
||||
|
||||
db, err := pgx.ConnectConfig(context.Background(), config)
|
||||
if err != nil {
|
||||
@ -253,7 +268,7 @@ func initDBPool(c *config) (*pgxpool.Pool, error) {
|
||||
config.ConnConfig.LogLevel = pgx.LogLevelNone
|
||||
}
|
||||
|
||||
config.ConnConfig.Logger = NewSQLLogger(*logger)
|
||||
config.ConnConfig.Logger = NewSQLLogger(logger)
|
||||
|
||||
// if c.DB.MaxRetries != 0 {
|
||||
// opt.MaxRetries = c.DB.MaxRetries
|
||||
@ -276,11 +291,11 @@ func initCompiler() {
|
||||
|
||||
qcompile, pcompile, err = initCompilers(conf)
|
||||
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 {
|
||||
logger.Fatal().Err(err).Msg("failed to initialized resolvers")
|
||||
errlog.Fatal().Err(err).Msg("failed to initialized resolvers")
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,7 +304,31 @@ func initConfOnce() {
|
||||
|
||||
if conf == 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cmdVersion(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("%s\n", BuildDetails())
|
||||
}
|
||||
|
||||
func BuildDetails() string {
|
||||
return fmt.Sprintf(`
|
||||
Super Graph %v
|
||||
For documentation, visit https://supergraph.dev
|
||||
|
||||
Commit SHA-1 : %v
|
||||
Commit timestamp : %v
|
||||
Branch : %v
|
||||
Go version : %v
|
||||
|
||||
Licensed under the Apache Public License 2.0
|
||||
Copyright 2015-2019 Vikram Rangnekar.
|
||||
`,
|
||||
version,
|
||||
lastCommitSHA,
|
||||
lastCommitTime,
|
||||
gitBranch,
|
||||
runtime.Version())
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
func cmdConfDump(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
cmd.Help()
|
||||
cmd.Help() //nolint: errcheck
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -17,11 +17,11 @@ func cmdConfDump(cmd *cobra.Command, args []string) {
|
||||
|
||||
conf, err := initConf()
|
||||
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 {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
logger.Info().Msgf("config dumped to ./%s", fname)
|
||||
|
@ -14,19 +14,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var sampleMigration = `-- This is a sample migration.
|
||||
|
||||
create table users(
|
||||
id serial primary key,
|
||||
fullname varchar not null,
|
||||
email varchar not null
|
||||
);
|
||||
|
||||
---- create above / drop below ----
|
||||
|
||||
drop table users;
|
||||
`
|
||||
|
||||
var newMigrationText = `-- Write your migrate up statements here
|
||||
|
||||
---- create above / drop below ----
|
||||
@ -48,8 +35,8 @@ func cmdDBSetup(cmd *cobra.Command, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) == false {
|
||||
logger.Fatal().Err(err).Msgf("unable to check if '%s' exists", sfile)
|
||||
if !os.IsNotExist(err) {
|
||||
errlog.Fatal().Err(err).Msgf("unable to check if '%s' exists", sfile)
|
||||
}
|
||||
|
||||
logger.Warn().Msgf("failed to read seed file '%s'", sfile)
|
||||
@ -59,7 +46,7 @@ func cmdDBReset(cmd *cobra.Command, args []string) {
|
||||
initConfOnce()
|
||||
|
||||
if conf.Production {
|
||||
logger.Fatal().Msg("db:reset does not work in production")
|
||||
errlog.Fatal().Msg("db:reset does not work in production")
|
||||
return
|
||||
}
|
||||
cmdDBDrop(cmd, []string{})
|
||||
@ -72,7 +59,7 @@ func cmdDBCreate(cmd *cobra.Command, args []string) {
|
||||
|
||||
conn, err := initDB(conf, false)
|
||||
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)
|
||||
|
||||
@ -80,7 +67,7 @@ func cmdDBCreate(cmd *cobra.Command, args []string) {
|
||||
|
||||
_, err = conn.Exec(ctx, sql)
|
||||
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)
|
||||
@ -92,7 +79,7 @@ func cmdDBDrop(cmd *cobra.Command, args []string) {
|
||||
|
||||
conn, err := initDB(conf, false)
|
||||
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)
|
||||
|
||||
@ -100,7 +87,7 @@ func cmdDBDrop(cmd *cobra.Command, args []string) {
|
||||
|
||||
_, err = conn.Exec(ctx, sql)
|
||||
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)
|
||||
@ -108,7 +95,7 @@ func cmdDBDrop(cmd *cobra.Command, args []string) {
|
||||
|
||||
func cmdDBNew(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
cmd.Help()
|
||||
cmd.Help() //nolint: errcheck
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -142,7 +129,7 @@ func cmdDBNew(cmd *cobra.Command, args []string) {
|
||||
|
||||
func cmdDBMigrate(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
cmd.Help()
|
||||
cmd.Help() //nolint: errcheck
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -151,24 +138,24 @@ func cmdDBMigrate(cmd *cobra.Command, args []string) {
|
||||
|
||||
conn, err := initDB(conf, true)
|
||||
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())
|
||||
|
||||
m, err := migrate.NewMigrator(conn, "schema_version")
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to initializing migrator")
|
||||
errlog.Fatal().Err(err).Msg("failed to initializing migrator")
|
||||
}
|
||||
|
||||
m.Data = getMigrationVars()
|
||||
|
||||
err = m.LoadMigrations(conf.MigrationsPath)
|
||||
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 {
|
||||
logger.Fatal().Msg("No migrations found")
|
||||
errlog.Fatal().Msg("No migrations found")
|
||||
}
|
||||
|
||||
m.OnStart = func(sequence int32, name, direction, sql string) {
|
||||
@ -187,7 +174,7 @@ func cmdDBMigrate(cmd *cobra.Command, args []string) {
|
||||
var n int64
|
||||
n, err = strconv.ParseInt(d, 10, 32)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("invalid destination")
|
||||
errlog.Fatal().Err(err).Msg("invalid destination")
|
||||
}
|
||||
return int32(n)
|
||||
}
|
||||
@ -211,24 +198,22 @@ func cmdDBMigrate(cmd *cobra.Command, args []string) {
|
||||
err = m.MigrateTo(currentVersion + mustParseDestination(dest[1:]))
|
||||
|
||||
} else {
|
||||
cmd.Help()
|
||||
cmd.Help() //nolint: errcheck
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Send()
|
||||
|
||||
// logger.Info().Err(err).Send()
|
||||
|
||||
// if err, ok := err.(m.MigrationPgError); ok {
|
||||
// if err.Detail != "" {
|
||||
// logger.Info().Err(err).Msg(err.Detail)
|
||||
// info.Err(err).Msg(err.Detail)
|
||||
// }
|
||||
|
||||
// if err.Position != 0 {
|
||||
// ele, err := ExtractErrorLine(err.Sql, int(err.Position))
|
||||
// if err != nil {
|
||||
// logger.Fatal().Err(err).Send()
|
||||
// errlog.Fatal().Err(err).Send()
|
||||
// }
|
||||
|
||||
// prefix := fmt.Sprintf()
|
||||
@ -247,29 +232,29 @@ func cmdDBStatus(cmd *cobra.Command, args []string) {
|
||||
|
||||
conn, err := initDB(conf, true)
|
||||
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())
|
||||
|
||||
m, err := migrate.NewMigrator(conn, "schema_version")
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to initialize migrator")
|
||||
errlog.Fatal().Err(err).Msg("failed to initialize migrator")
|
||||
}
|
||||
|
||||
m.Data = getMigrationVars()
|
||||
|
||||
err = m.LoadMigrations(conf.MigrationsPath)
|
||||
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 {
|
||||
logger.Fatal().Msg("no migrations found")
|
||||
errlog.Fatal().Msg("no migrations found")
|
||||
}
|
||||
|
||||
mver, err := m.GetCurrentVersion()
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to retrieve migration")
|
||||
errlog.Fatal().Err(err).Msg("failed to retrieve migration")
|
||||
}
|
||||
|
||||
var status string
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
|
||||
func cmdNew(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
cmd.Help()
|
||||
cmd.Help() //nolint: errcheck
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -115,13 +115,17 @@ func (t *Templ) get(name string) ([]byte, error) {
|
||||
b := bytes.Buffer{}
|
||||
tmpl := fasttemplate.New(v, "{%", "%}")
|
||||
|
||||
tmpl.ExecuteFunc(&b, func(w io.Writer, tag string) (int, error) {
|
||||
_, err := tmpl.ExecuteFunc(&b, func(w io.Writer, tag string) (int, error) {
|
||||
if val, ok := t.data[strings.TrimSpace(tag)]; ok {
|
||||
return w.Write([]byte(val))
|
||||
}
|
||||
return 0, fmt.Errorf("unknown template variable '%s'", tag)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
@ -133,13 +137,13 @@ func ifNotExists(filePath string, doFn func(string) error) {
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) == false {
|
||||
logger.Fatal().Err(err).Msgf("unable to check if '%s' exists", filePath)
|
||||
if !os.IsNotExist(err) {
|
||||
errlog.Fatal().Err(err).Msgf("unable to check if '%s' exists", filePath)
|
||||
}
|
||||
|
||||
err = doFn(filePath)
|
||||
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)
|
||||
|
@ -20,14 +20,14 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
if conf, err = initConf(); err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to read config")
|
||||
errlog.Fatal().Err(err).Msg("failed to read config")
|
||||
}
|
||||
|
||||
conf.Production = false
|
||||
|
||||
db, err = initDBPool(conf)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to connect to database")
|
||||
errlog.Fatal().Err(err).Msg("failed to connect to database")
|
||||
}
|
||||
|
||||
initCompiler()
|
||||
@ -36,14 +36,14 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
|
||||
|
||||
b, err := ioutil.ReadFile(path.Join(confPath, conf.SeedFile))
|
||||
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.Set("graphql", graphQLFunc)
|
||||
|
||||
console := vm.NewObject()
|
||||
console.Set("log", logFunc)
|
||||
console.Set("log", logFunc) //nolint: errcheck
|
||||
vm.Set("console", console)
|
||||
|
||||
fake := vm.NewObject()
|
||||
@ -52,7 +52,7 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
|
||||
|
||||
_, err = vm.RunScript("seed.js", string(b))
|
||||
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")
|
||||
@ -60,15 +60,15 @@ func cmdDBSeed(cmd *cobra.Command, args []string) {
|
||||
|
||||
//func runFunc(call goja.FunctionCall) {
|
||||
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 {
|
||||
panic(err)
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
c := context.Background()
|
||||
|
||||
if v, ok := opt["user_id"]; ok && len(v) != 0 {
|
||||
ctx = context.WithValue(ctx, userIDKey, v)
|
||||
c = context.WithValue(c, userIDKey, v)
|
||||
}
|
||||
|
||||
var role string
|
||||
@ -79,67 +79,56 @@ func graphQLFunc(query string, data interface{}, opt map[string]string) map[stri
|
||||
role = "user"
|
||||
}
|
||||
|
||||
c := &coreContext{Context: ctx}
|
||||
c.req.Query = query
|
||||
c.req.Vars = b
|
||||
|
||||
st, err := c.buildStmtByRole(role)
|
||||
stmts, err := buildRoleStmt([]byte(query), vars, role)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("graphql query failed: %s", err))
|
||||
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))
|
||||
|
||||
if err == errNoUserID {
|
||||
panic(fmt.Errorf("query requires a user_id"))
|
||||
}
|
||||
_, err = t.ExecuteFunc(buf, argMap(c, vars))
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
finalSQL := buf.String()
|
||||
|
||||
tx, err := db.Begin(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
defer tx.Rollback(c)
|
||||
defer tx.Rollback(c) //nolint: errcheck
|
||||
|
||||
if conf.DB.SetUserID {
|
||||
if err := c.setLocalUserID(tx); err != nil {
|
||||
panic(err)
|
||||
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 {
|
||||
panic(fmt.Errorf("sql query failed: %s", err))
|
||||
errlog.Fatal().Err(err).Msg("sql query failed")
|
||||
}
|
||||
|
||||
if err := tx.Commit(c); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res, err := c.execRemoteJoin(st.qc, st.skipped, root)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
val := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(res, &val)
|
||||
err = json.Unmarshal(root, &val)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to deserialize json: %s", err))
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
//nolint: errcheck
|
||||
func logFunc(args ...interface{}) {
|
||||
for _, arg := range args {
|
||||
if _, ok := arg.(map[string]interface{}); ok {
|
||||
@ -156,6 +145,7 @@ func logFunc(args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint: errcheck
|
||||
func setFakeFuncs(f *goja.Object) {
|
||||
gofakeit.Seed(0)
|
||||
|
||||
|
@ -8,12 +8,12 @@ func cmdServ(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to connect to database")
|
||||
errlog.Fatal().Err(err).Msg("failed to connect to database")
|
||||
}
|
||||
|
||||
initCompiler()
|
||||
|
@ -64,17 +64,12 @@ type config struct {
|
||||
User string
|
||||
Password string
|
||||
Schema string
|
||||
PoolSize int32 `mapstructure:"pool_size"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
SetUserID bool `mapstructure:"set_user_id"`
|
||||
PoolSize int32 `mapstructure:"pool_size"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
SetUserID bool `mapstructure:"set_user_id"`
|
||||
|
||||
Vars map[string]string `mapstructure:"variables"`
|
||||
|
||||
Defaults struct {
|
||||
Filters []string
|
||||
Blocklist []string
|
||||
}
|
||||
Vars map[string]string `mapstructure:"variables"`
|
||||
Blocklist []string
|
||||
|
||||
Tables []configTable
|
||||
} `mapstructure:"database"`
|
||||
@ -83,6 +78,7 @@ type config struct {
|
||||
|
||||
RolesQuery string `mapstructure:"roles_query"`
|
||||
Roles []configRole
|
||||
roles map[string]*configRole
|
||||
}
|
||||
|
||||
type configTable struct {
|
||||
@ -177,9 +173,10 @@ func newConfig(name string) *viper.Viper {
|
||||
vi.SetDefault("database.schema", "public")
|
||||
|
||||
vi.SetDefault("env", "development")
|
||||
vi.BindEnv("env", "GO_ENV")
|
||||
vi.BindEnv("HOST", "HOST")
|
||||
vi.BindEnv("PORT", "PORT")
|
||||
|
||||
vi.BindEnv("env", "GO_ENV") //nolint: errcheck
|
||||
vi.BindEnv("HOST", "HOST") //nolint: errcheck
|
||||
vi.BindEnv("PORT", "PORT") //nolint: errcheck
|
||||
|
||||
vi.SetDefault("auth.rails.max_idle", 80)
|
||||
vi.SetDefault("auth.rails.max_active", 12000)
|
||||
@ -221,16 +218,15 @@ func (c *config) Init(vi *viper.Viper) error {
|
||||
}
|
||||
|
||||
c.RolesQuery = sanitize(c.RolesQuery)
|
||||
|
||||
rolesMap := make(map[string]struct{})
|
||||
c.roles = make(map[string]*configRole)
|
||||
|
||||
for i := range c.Roles {
|
||||
role := &c.Roles[i]
|
||||
|
||||
if _, ok := rolesMap[role.Name]; ok {
|
||||
logger.Fatal().Msgf("duplicate role '%s' found", role.Name)
|
||||
if _, ok := c.roles[role.Name]; ok {
|
||||
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.tablesMap = make(map[string]*configRoleTable)
|
||||
|
||||
@ -238,14 +234,16 @@ func (c *config) Init(vi *viper.Viper) error {
|
||||
role.tablesMap[table.Name] = &role.Tables[n]
|
||||
}
|
||||
|
||||
rolesMap[role.Name] = struct{}{}
|
||||
c.roles[role.Name] = role
|
||||
}
|
||||
|
||||
if _, ok := rolesMap["user"]; !ok {
|
||||
c.Roles = append(c.Roles, configRole{Name: "user"})
|
||||
if _, ok := c.roles["user"]; !ok {
|
||||
u := configRole{Name: "user"}
|
||||
c.Roles = append(c.Roles, u)
|
||||
c.roles["user"] = &u
|
||||
}
|
||||
|
||||
if _, ok := rolesMap["anon"]; !ok {
|
||||
if _, ok := c.roles["anon"]; !ok {
|
||||
logger.Warn().Msg("unauthenticated requests will be blocked. no role 'anon' defined")
|
||||
c.AuthFailBlock = true
|
||||
}
|
||||
@ -262,7 +260,7 @@ func (c *config) validate() {
|
||||
name := c.Roles[i].Name
|
||||
|
||||
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{}{}
|
||||
}
|
||||
@ -273,7 +271,7 @@ func (c *config) validate() {
|
||||
name := c.Tables[i].Name
|
||||
|
||||
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{}{}
|
||||
}
|
||||
|
368
serv/core.go
368
serv/core.go
@ -8,20 +8,14 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/valyala/fasttemplate"
|
||||
)
|
||||
|
||||
const (
|
||||
empty = ""
|
||||
)
|
||||
|
||||
type coreContext struct {
|
||||
req gqlReq
|
||||
res gqlResp
|
||||
@ -32,6 +26,10 @@ func (c *coreContext) handleReq(w io.Writer, req *http.Request) error {
|
||||
c.req.ref = req.Referer()
|
||||
c.req.hdr = req.Header
|
||||
|
||||
if len(c.req.Vars) == 2 {
|
||||
c.req.Vars = nil
|
||||
}
|
||||
|
||||
if authCheck(c) {
|
||||
c.req.role = "user"
|
||||
} else {
|
||||
@ -47,83 +45,38 @@ func (c *coreContext) handleReq(w io.Writer, req *http.Request) error {
|
||||
}
|
||||
|
||||
func (c *coreContext) execQuery() ([]byte, error) {
|
||||
var err error
|
||||
var skipped uint32
|
||||
var qc *qcode.QCode
|
||||
var data []byte
|
||||
var st *stmt
|
||||
var err error
|
||||
|
||||
if conf.Production {
|
||||
var ps *preparedItem
|
||||
|
||||
data, ps, err = c.resolvePreparedSQL()
|
||||
data, st, err = c.resolvePreparedSQL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("default_role", c.req.role).
|
||||
Msg(c.req.Query)
|
||||
|
||||
skipped = ps.skipped
|
||||
qc = ps.qc
|
||||
return nil, errors.New("query failed. check logs for error")
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
data, skipped, err = c.resolveSQL()
|
||||
if err != nil {
|
||||
if data, st, err = c.resolveSQL(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c.execRemoteJoin(qc, skipped, data)
|
||||
return execRemoteJoin(st, data, c.req.hdr)
|
||||
}
|
||||
|
||||
func (c *coreContext) execRemoteJoin(qc *qcode.QCode, skipped uint32, data []byte) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
if len(data) == 0 || skipped == 0 {
|
||||
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) {
|
||||
var tx pgx.Tx
|
||||
var err error
|
||||
|
||||
mutation := isMutation(c.req.Query)
|
||||
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
|
||||
|
||||
@ -131,11 +84,11 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) {
|
||||
if tx, err = db.Begin(c); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer tx.Rollback(c)
|
||||
defer tx.Rollback(c) //nolint: errcheck
|
||||
}
|
||||
|
||||
if conf.DB.SetUserID {
|
||||
if err := c.setLocalUserID(tx); err != nil {
|
||||
if err := setLocalUserID(c, tx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
@ -150,7 +103,7 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) {
|
||||
} else if v := c.Value(userRoleKey); v != nil {
|
||||
role = v.(string)
|
||||
|
||||
} else if mutation {
|
||||
} else {
|
||||
role = c.req.role
|
||||
|
||||
}
|
||||
@ -162,21 +115,29 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) {
|
||||
|
||||
var root []byte
|
||||
var row pgx.Row
|
||||
vars := argList(c, ps.args)
|
||||
|
||||
if useTx {
|
||||
row = tx.QueryRow(c, ps.stmt.SQL, vars...)
|
||||
} else {
|
||||
row = db.QueryRow(c, ps.stmt.SQL, vars...)
|
||||
vars, err := argList(c, ps.args)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if mutation {
|
||||
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)
|
||||
}
|
||||
|
||||
logger.Debug().Str("default_role", c.req.role).Str("role", role).Msg(c.req.Query)
|
||||
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
|
||||
@ -190,65 +151,55 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return root, ps, nil
|
||||
return root, ps.st, nil
|
||||
}
|
||||
|
||||
func (c *coreContext) resolveSQL() ([]byte, uint32, error) {
|
||||
func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
|
||||
var tx pgx.Tx
|
||||
var err error
|
||||
|
||||
mutation := isMutation(c.req.Query)
|
||||
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, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
defer tx.Rollback(c)
|
||||
defer tx.Rollback(c) //nolint: errcheck
|
||||
}
|
||||
|
||||
if conf.DB.SetUserID {
|
||||
if err := c.setLocalUserID(tx); err != nil {
|
||||
return nil, 0, err
|
||||
if err := setLocalUserID(c, tx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if useRoleQuery {
|
||||
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 {
|
||||
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 {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var st *stmt
|
||||
|
||||
if mutation {
|
||||
st = findStmt(c.req.role, stmts)
|
||||
} else {
|
||||
st = &stmts[0]
|
||||
return nil, nil, err
|
||||
}
|
||||
st := &stmts[0]
|
||||
|
||||
t := fasttemplate.New(st.sql, openVar, closeVar)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
_, err = t.ExecuteFunc(buf, argMap(c))
|
||||
|
||||
if err == errNoUserID {
|
||||
logger.Warn().Msg("no user id found. query requires an authenicated request")
|
||||
}
|
||||
|
||||
_, err = t.ExecuteFunc(buf, argMap(c, c.req.Vars))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
finalSQL := buf.String()
|
||||
|
||||
var stime time.Time
|
||||
@ -258,195 +209,56 @@ func (c *coreContext) resolveSQL() ([]byte, uint32, error) {
|
||||
}
|
||||
|
||||
var root []byte
|
||||
var role, defaultRole string
|
||||
var role string
|
||||
var row pgx.Row
|
||||
|
||||
defaultRole := c.req.role
|
||||
|
||||
if useTx {
|
||||
row = tx.QueryRow(c, finalSQL)
|
||||
} else {
|
||||
row = db.QueryRow(c, finalSQL)
|
||||
}
|
||||
|
||||
if mutation {
|
||||
if len(stmts) == 1 {
|
||||
err = row.Scan(&root)
|
||||
|
||||
} else {
|
||||
err = row.Scan(&role, &root)
|
||||
defaultRole = c.req.role
|
||||
c.req.role = role
|
||||
|
||||
}
|
||||
|
||||
logger.Debug().Str("default_role", defaultRole).Str("role", role).Msg(c.req.Query)
|
||||
if len(role) == 0 {
|
||||
logger.Debug().Str("default_role", defaultRole).Msg(c.req.Query)
|
||||
} else {
|
||||
logger.Debug().Str("default_role", defaultRole).Str("role", role).Msg(c.req.Query)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if useTx {
|
||||
if err := tx.Commit(c); err != nil {
|
||||
return nil, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if conf.EnableTracing && len(st.qc.Selects) != 0 {
|
||||
if !conf.Production {
|
||||
_allowList.add(&c.req)
|
||||
}
|
||||
|
||||
if len(stmts) > 1 {
|
||||
if st = findStmt(role, stmts); st == nil {
|
||||
return nil, nil, fmt.Errorf("invalid role '%s' returned", role)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.EnableTracing {
|
||||
for _, id := range st.qc.Roots {
|
||||
c.addTrace(st.qc.Selects, id, stime)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Production == false {
|
||||
_allowList.add(&c.req)
|
||||
}
|
||||
|
||||
return root, st.skipped, nil
|
||||
}
|
||||
|
||||
func (c *coreContext) resolveRemote(
|
||||
hdr http.Header,
|
||||
h *xxhash.Digest,
|
||||
field jsn.Field,
|
||||
sel []qcode.Select,
|
||||
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
|
||||
|
||||
// replacement data for the marked insertion points
|
||||
// key and value will be replaced by whats below
|
||||
toA := [1]jsn.Field{}
|
||||
to := toA[:1]
|
||||
|
||||
// use the json key to find the related Select object
|
||||
k1 := xxhash.Sum64(field.Key)
|
||||
|
||||
s, ok := sfmap[k1]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
p := sel[s.ParentID]
|
||||
|
||||
// then use the Table nme in the Select and it's parent
|
||||
// to find the resolver to use for this relationship
|
||||
k2 := mkkey(h, s.Table, p.Table)
|
||||
|
||||
r, ok := rmap[k2]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
id := jsn.Value(field.Value)
|
||||
if len(id) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
st := time.Now()
|
||||
|
||||
b, err := r.Fn(hdr, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if conf.EnableTracing {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else {
|
||||
ob.WriteString("null")
|
||||
}
|
||||
|
||||
to[0] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
|
||||
return to, nil
|
||||
}
|
||||
|
||||
func (c *coreContext) resolveRemotes(
|
||||
hdr http.Header,
|
||||
h *xxhash.Digest,
|
||||
from []jsn.Field,
|
||||
sel []qcode.Select,
|
||||
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
|
||||
|
||||
// replacement data for the marked insertion points
|
||||
// key and value will be replaced by whats below
|
||||
to := make([]jsn.Field, len(from))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(from))
|
||||
|
||||
var cerr error
|
||||
|
||||
for i, id := range from {
|
||||
|
||||
// use the json key to find the related Select object
|
||||
k1 := xxhash.Sum64(id.Key)
|
||||
|
||||
s, ok := sfmap[k1]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
p := sel[s.ParentID]
|
||||
|
||||
// then use the Table nme in the Select and it's parent
|
||||
// to find the resolver to use for this relationship
|
||||
k2 := mkkey(h, s.Table, p.Table)
|
||||
|
||||
r, ok := rmap[k2]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
id := jsn.Value(id.Value)
|
||||
if len(id) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
go func(n int, id []byte, s *qcode.Select) {
|
||||
defer wg.Done()
|
||||
|
||||
st := time.Now()
|
||||
|
||||
b, err := r.Fn(hdr, id)
|
||||
if err != nil {
|
||||
cerr = fmt.Errorf("%s: %s", s.Table, err)
|
||||
return
|
||||
}
|
||||
|
||||
if conf.EnableTracing {
|
||||
c.addTrace(sel, s.ID, st)
|
||||
}
|
||||
|
||||
if len(r.Path) != 0 {
|
||||
b = jsn.Strip(b, r.Path)
|
||||
}
|
||||
|
||||
var ob bytes.Buffer
|
||||
|
||||
if len(s.Cols) != 0 {
|
||||
err = jsn.Filter(&ob, b, colsToList(s.Cols))
|
||||
if err != nil {
|
||||
cerr = fmt.Errorf("%s: %s", s.Table, err)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
ob.WriteString("null")
|
||||
}
|
||||
|
||||
to[n] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
|
||||
}(i, id, s)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return to, cerr
|
||||
return root, st, nil
|
||||
}
|
||||
|
||||
func (c *coreContext) executeRoleQuery(tx pgx.Tx) (string, error) {
|
||||
@ -460,15 +272,6 @@ func (c *coreContext) executeRoleQuery(tx pgx.Tx) (string, error) {
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (c *coreContext) setLocalUserID(tx pgx.Tx) error {
|
||||
var err error
|
||||
if v := c.Value(userIDKey); v != nil {
|
||||
_, err = tx.Exec(c, fmt.Sprintf(`SET LOCAL "user.id" = %s;`, v))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *coreContext) render(w io.Writer, data []byte) error {
|
||||
c.res.Data = json.RawMessage(data)
|
||||
return json.NewEncoder(w).Encode(c.res)
|
||||
@ -497,7 +300,7 @@ func (c *coreContext) addTrace(sel []qcode.Select, id int32, st time.Time) {
|
||||
|
||||
n--
|
||||
for i := id; ; i = sel[i].ParentID {
|
||||
path[n] = sel[i].Table
|
||||
path[n] = sel[i].Name
|
||||
if sel[i].ParentID == -1 {
|
||||
break
|
||||
}
|
||||
@ -507,7 +310,7 @@ func (c *coreContext) addTrace(sel []qcode.Select, id int32, st time.Time) {
|
||||
tr := resolver{
|
||||
Path: path,
|
||||
ParentType: "Query",
|
||||
FieldName: sel[id].Table,
|
||||
FieldName: sel[id].Name,
|
||||
ReturnType: "object",
|
||||
StartOffset: 1,
|
||||
Duration: du,
|
||||
@ -541,12 +344,12 @@ func parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) (
|
||||
for i := range sel {
|
||||
s := &sel[i]
|
||||
|
||||
if isSkipped(skipped, uint32(s.ID)) == false {
|
||||
if !isSkipped(skipped, uint32(s.ID)) {
|
||||
continue
|
||||
}
|
||||
|
||||
p := sel[s.ParentID]
|
||||
k := mkkey(h, s.Table, p.Table)
|
||||
k := mkkey(h, s.Name, p.Name)
|
||||
|
||||
if r, ok := rmap[k]; ok {
|
||||
fm[n] = r.IDField
|
||||
@ -560,6 +363,15 @@ func parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) (
|
||||
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 {
|
||||
return ((n & (1 << pos)) != 0)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/dosco/super-graph/psql"
|
||||
@ -17,172 +18,170 @@ type stmt struct {
|
||||
sql string
|
||||
}
|
||||
|
||||
func (c *coreContext) buildStmt() ([]stmt, error) {
|
||||
var vars map[string]json.RawMessage
|
||||
func buildStmt(qt qcode.QType, gql, vars []byte, role string) ([]stmt, error) {
|
||||
switch qt {
|
||||
case qcode.QTMutation:
|
||||
return buildRoleStmt(gql, vars, role)
|
||||
|
||||
if len(c.req.Vars) != 0 {
|
||||
if err := json.Unmarshal(c.req.Vars, &vars); err != nil {
|
||||
case qcode.QTQuery:
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
gql := []byte(c.req.Query)
|
||||
|
||||
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)
|
||||
qc, err := qcompile.Compile(gql, ro.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmts := make([]stmt, 0, len(conf.Roles))
|
||||
mutation := (qc.Type != qcode.QTQuery)
|
||||
// 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) {
|
||||
return nil, errors.New("query contains tables with no 'anon' role config")
|
||||
}
|
||||
|
||||
stmts := []stmt{stmt{role: ro, qc: qc}}
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
for i := 1; i < len(conf.Roles); i++ {
|
||||
skipped, err := pcompile.Compile(qc, w, psql.Variables(vm))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmts[0].skipped = skipped
|
||||
stmts[0].sql = w.String()
|
||||
|
||||
return stmts, nil
|
||||
}
|
||||
|
||||
func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
|
||||
var vm map[string]json.RawMessage
|
||||
var err error
|
||||
|
||||
if len(vars) != 0 {
|
||||
if err := json.Unmarshal(vars, &vm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(conf.RolesQuery) == 0 {
|
||||
return buildRoleStmt(gql, vars, "user")
|
||||
}
|
||||
|
||||
stmts := make([]stmt, 0, len(conf.Roles))
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
for i := 0; i < len(conf.Roles); i++ {
|
||||
role := &conf.Roles[i]
|
||||
|
||||
// For mutations only render sql for a single role from the request
|
||||
if mutation && len(c.req.role) != 0 && role.Name != c.req.role {
|
||||
continue
|
||||
}
|
||||
|
||||
qc, err = qcompile.Compile(gql, role.Name)
|
||||
qc, err := qcompile.Compile(gql, role.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if conf.Production && role.Name == "anon" {
|
||||
for _, id := range qc.Roots {
|
||||
root := qc.Selects[id]
|
||||
if _, ok := role.tablesMap[root.Table]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stmts = append(stmts, stmt{role: role, qc: qc})
|
||||
|
||||
if mutation {
|
||||
skipped, err := pcompile.Compile(qc, w, psql.Variables(vars))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &stmts[len(stmts)-1]
|
||||
s.skipped = skipped
|
||||
s.sql = w.String()
|
||||
w.Reset()
|
||||
skipped, err := pcompile.Compile(qc, w, psql.Variables(vm))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &stmts[len(stmts)-1]
|
||||
s.skipped = skipped
|
||||
s.sql = w.String()
|
||||
w.Reset()
|
||||
}
|
||||
|
||||
if mutation {
|
||||
return stmts, nil
|
||||
sql, err := renderUserQuery(stmts, vm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmts[0].sql = sql
|
||||
return stmts, nil
|
||||
}
|
||||
|
||||
//nolint: errcheck
|
||||
func renderUserQuery(
|
||||
stmts []stmt, vars map[string]json.RawMessage) (string, error) {
|
||||
|
||||
var err error
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
io.WriteString(w, `SELECT "_sg_auth_info"."role", (CASE "_sg_auth_info"."role" `)
|
||||
|
||||
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, s.role.Name)
|
||||
io.WriteString(w, `' THEN (`)
|
||||
|
||||
s.skipped, err = pcompile.Compile(s.qc, w, psql.Variables(vars))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
io.WriteString(w, `) `)
|
||||
}
|
||||
io.WriteString(w, `END) FROM (`)
|
||||
|
||||
if len(conf.RolesQuery) == 0 {
|
||||
v := c.Value(userRoleKey)
|
||||
io.WriteString(w, `END) FROM (SELECT (CASE WHEN EXISTS (`)
|
||||
io.WriteString(w, conf.RolesQuery)
|
||||
io.WriteString(w, `) THEN `)
|
||||
|
||||
io.WriteString(w, `VALUES ("`)
|
||||
if v != nil {
|
||||
io.WriteString(w, v.(string))
|
||||
} else {
|
||||
io.WriteString(w, c.req.role)
|
||||
io.WriteString(w, `(SELECT (CASE`)
|
||||
for _, s := range stmts {
|
||||
if len(s.role.Match) == 0 {
|
||||
continue
|
||||
}
|
||||
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, `) THEN `)
|
||||
|
||||
io.WriteString(w, `(SELECT (CASE`)
|
||||
for _, s := range stmts {
|
||||
if len(s.role.Match) == 0 {
|
||||
continue
|
||||
}
|
||||
io.WriteString(w, ` WHEN `)
|
||||
io.WriteString(w, s.role.Match)
|
||||
io.WriteString(w, ` THEN '`)
|
||||
io.WriteString(w, s.role.Name)
|
||||
io.WriteString(w, `'`)
|
||||
}
|
||||
|
||||
if len(c.req.role) == 0 {
|
||||
io.WriteString(w, ` ELSE 'anon' END) FROM (`)
|
||||
} else {
|
||||
io.WriteString(w, ` ELSE '`)
|
||||
io.WriteString(w, c.req.role)
|
||||
io.WriteString(w, `' END) FROM (`)
|
||||
}
|
||||
|
||||
io.WriteString(w, conf.RolesQuery)
|
||||
io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) ELSE '`)
|
||||
if len(c.req.role) == 0 {
|
||||
io.WriteString(w, `anon`)
|
||||
} else {
|
||||
io.WriteString(w, c.req.role)
|
||||
}
|
||||
io.WriteString(w, `' END) FROM (VALUES (1)) AS "_sg_auth_filler") AS "_sg_auth_info"(role) LIMIT 1; `)
|
||||
io.WriteString(w, ` WHEN `)
|
||||
io.WriteString(w, s.role.Match)
|
||||
io.WriteString(w, ` THEN '`)
|
||||
io.WriteString(w, s.role.Name)
|
||||
io.WriteString(w, `'`)
|
||||
}
|
||||
|
||||
stmts[0].sql = w.String()
|
||||
stmts[0].role = nil
|
||||
io.WriteString(w, ` ELSE 'user' END) FROM (`)
|
||||
io.WriteString(w, conf.RolesQuery)
|
||||
io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) `)
|
||||
io.WriteString(w, `ELSE 'anon' END) FROM (VALUES (1)) AS "_sg_auth_filler") AS "_sg_auth_info"(role) LIMIT 1; `)
|
||||
|
||||
return stmts, nil
|
||||
return w.String(), nil
|
||||
}
|
||||
|
||||
func (c *coreContext) buildStmtByRole(role string) (stmt, error) {
|
||||
var st stmt
|
||||
var err error
|
||||
|
||||
if len(role) == 0 {
|
||||
return st, errors.New(`no role defined`)
|
||||
}
|
||||
|
||||
var vars map[string]json.RawMessage
|
||||
|
||||
if len(c.req.Vars) != 0 {
|
||||
if err := json.Unmarshal(c.req.Vars, &vars); err != nil {
|
||||
return st, err
|
||||
func hasTablesWithConfig(qc *qcode.QCode, role *configRole) bool {
|
||||
for _, id := range qc.Roots {
|
||||
t, err := schema.GetTable(qc.Selects[id].Name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if _, ok := role.tablesMap[t.Name]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
gql := []byte(c.req.Query)
|
||||
|
||||
st.qc, err = qcompile.Compile(gql, role)
|
||||
if err != nil {
|
||||
return st, err
|
||||
}
|
||||
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
st.skipped, err = pcompile.Compile(st.qc, w, psql.Variables(vars))
|
||||
if err != nil {
|
||||
return st, err
|
||||
}
|
||||
|
||||
st.sql = w.String()
|
||||
|
||||
return st, nil
|
||||
|
||||
return true
|
||||
}
|
||||
|
197
serv/core_remote.go
Normal file
197
serv/core_remote.go
Normal file
@ -0,0 +1,197 @@
|
||||
package serv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
)
|
||||
|
||||
func execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
if len(data) == 0 || st.skipped == 0 {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
sel := st.qc.Selects
|
||||
h := xxhash.New()
|
||||
|
||||
// fetch the field name used within the db response json
|
||||
// that are used to mark insertion points and the mapping between
|
||||
// those field names and their select objects
|
||||
fids, sfmap := parentFieldIds(h, sel, st.skipped)
|
||||
|
||||
// fetch the field values of the marked insertion points
|
||||
// these values contain the id to be used with fetching remote data
|
||||
from := jsn.Get(data, fids)
|
||||
var to []jsn.Field
|
||||
|
||||
switch {
|
||||
case len(from) == 1:
|
||||
to, err = resolveRemote(hdr, h, from[0], sel, sfmap)
|
||||
|
||||
case len(from) > 1:
|
||||
to, err = resolveRemotes(hdr, h, from, sel, sfmap)
|
||||
|
||||
default:
|
||||
return nil, errors.New("something wrong no remote ids found in db response")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ob bytes.Buffer
|
||||
|
||||
err = jsn.Replace(&ob, data, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ob.Bytes(), nil
|
||||
}
|
||||
|
||||
func resolveRemote(
|
||||
hdr http.Header,
|
||||
h *xxhash.Digest,
|
||||
field jsn.Field,
|
||||
sel []qcode.Select,
|
||||
sfmap map[uint64]*qcode.Select) ([]jsn.Field, error) {
|
||||
|
||||
// replacement data for the marked insertion points
|
||||
// key and value will be replaced by whats below
|
||||
toA := [1]jsn.Field{}
|
||||
to := toA[:1]
|
||||
|
||||
// use the json key to find the related Select object
|
||||
k1 := xxhash.Sum64(field.Key)
|
||||
|
||||
s, ok := sfmap[k1]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
p := sel[s.ParentID]
|
||||
|
||||
// then use the Table nme in the Select and it's parent
|
||||
// to find the resolver to use for this relationship
|
||||
k2 := mkkey(h, s.Name, p.Name)
|
||||
|
||||
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.Name, p.Name)
|
||||
|
||||
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.Name, 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.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
ob.WriteString("null")
|
||||
}
|
||||
|
||||
to[n] = jsn.Field{Key: []byte(s.FieldName), Value: ob.Bytes()}
|
||||
}(i, id, s)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return to, cerr
|
||||
}
|
@ -4,7 +4,7 @@ package serv
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
gql := string(data)
|
||||
isMutation(gql)
|
||||
gqlName(gql)
|
||||
gqlHash(gql, nil, "")
|
||||
|
||||
return 1
|
||||
|
@ -10,7 +10,7 @@ func TestFuzzCrashers(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, f := range crashers {
|
||||
isMutation(f)
|
||||
_ = gqlName(f)
|
||||
gqlHash(f, nil, "")
|
||||
}
|
||||
}
|
||||
|
17
serv/http.go
17
serv/http.go
@ -8,8 +8,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -20,8 +18,6 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
upgrader = websocket.Upgrader{}
|
||||
errNoUserID = errors.New("no user_id available")
|
||||
errUnauthorized = errors.New("not authorized")
|
||||
)
|
||||
|
||||
@ -34,8 +30,6 @@ type gqlReq struct {
|
||||
hdr http.Header
|
||||
}
|
||||
|
||||
type variables map[string]json.RawMessage
|
||||
|
||||
type gqlResp struct {
|
||||
Error string `json:"message,omitempty"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
@ -70,7 +64,8 @@ type resolver struct {
|
||||
func apiv1Http(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := &coreContext{Context: r.Context()}
|
||||
|
||||
if conf.AuthFailBlock && authCheck(ctx) == false {
|
||||
//nolint: errcheck
|
||||
if conf.AuthFailBlock && !authCheck(ctx) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(gqlResp{Error: errUnauthorized.Error()})
|
||||
return
|
||||
@ -78,7 +73,7 @@ func apiv1Http(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
b, err := ioutil.ReadAll(io.LimitReader(r.Body, maxReadBytes))
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -86,7 +81,7 @@ func apiv1Http(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
err = json.Unmarshal(b, &ctx.req)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -98,6 +93,7 @@ func apiv1Http(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
err = ctx.handleReq(w, r)
|
||||
|
||||
//nolint: errcheck
|
||||
if err == errUnauthorized {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(gqlResp{Error: err.Error()})
|
||||
@ -105,12 +101,13 @@ func apiv1Http(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Err(err).Msg("failed to handle request")
|
||||
errlog.Error().Err(err).Msg("failed to handle request")
|
||||
errorResp(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//nolint: errcheck
|
||||
func errorResp(w http.ResponseWriter, err error) {
|
||||
json.NewEncoder(w).Encode(gqlResp{Error: err.Error()})
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package serv
|
||||
|
||||
import "net/http"
|
||||
|
||||
//nolint: errcheck
|
||||
func introspect(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{
|
||||
|
165
serv/prepare.go
165
serv/prepare.go
@ -3,7 +3,6 @@ package serv
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@ -14,10 +13,9 @@ import (
|
||||
)
|
||||
|
||||
type preparedItem struct {
|
||||
stmt *pgconn.StatementDescription
|
||||
args [][]byte
|
||||
skipped uint32
|
||||
qc *qcode.QCode
|
||||
sd *pgconn.StatementDescription
|
||||
args [][]byte
|
||||
st *stmt
|
||||
}
|
||||
|
||||
var (
|
||||
@ -25,85 +23,126 @@ var (
|
||||
)
|
||||
|
||||
func initPreparedList() {
|
||||
ctx := context.Background()
|
||||
|
||||
tx, err := db.Begin(ctx)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
c := context.Background()
|
||||
_preparedList = make(map[string]*preparedItem)
|
||||
|
||||
if err := prepareRoleStmt(ctx, tx); err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to prepare get role statement")
|
||||
tx, err := db.Begin(c)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
defer tx.Rollback(c) //nolint: errcheck
|
||||
|
||||
err = prepareRoleStmt(c, tx)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to prepare get role statement")
|
||||
}
|
||||
|
||||
if err := tx.Commit(c); err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
success := 0
|
||||
|
||||
for _, v := range _allowList.list {
|
||||
err := prepareStmt(ctx, tx, v.gql, v.vars)
|
||||
if err != nil {
|
||||
logger.Warn().Str("gql", v.gql).Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
logger.Info().Msgf("Registered %d queries from allow.list as prepared statements", len(_allowList.list))
|
||||
}
|
||||
|
||||
func prepareStmt(ctx context.Context, tx pgx.Tx, 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 {
|
||||
if len(v.gql) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
finalSQL, am := processTemplate(s.sql)
|
||||
err := prepareStmt(c, v.gql, v.vars)
|
||||
if err == nil {
|
||||
success++
|
||||
continue
|
||||
}
|
||||
|
||||
pstmt, err := tx.Prepare(c.Context, "", finalSQL)
|
||||
if len(v.vars) == 0 {
|
||||
logger.Warn().Err(err).Msg(v.gql)
|
||||
} else {
|
||||
logger.Warn().Err(err).Msgf("%s %s", v.vars, v.gql)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Msgf("Registered %d of %d queries from allow.list as prepared statements",
|
||||
success, len(_allowList.list))
|
||||
}
|
||||
|
||||
func prepareStmt(c context.Context, gql string, vars []byte) error {
|
||||
qt := qcode.GetQType(gql)
|
||||
q := []byte(gql)
|
||||
|
||||
tx, err := db.Begin(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(c) //nolint: errcheck
|
||||
|
||||
switch qt {
|
||||
case qcode.QTQuery:
|
||||
stmts1, err := buildMultiStmt(q, vars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var key string
|
||||
|
||||
if s.role == nil {
|
||||
key = gqlHash(gql, c.req.Vars, "")
|
||||
} else {
|
||||
key = gqlHash(gql, c.req.Vars, s.role.Name)
|
||||
err = prepare(c, tx, &stmts1[0], gqlHash(gql, vars, "user"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_preparedList[key] = &preparedItem{
|
||||
stmt: pstmt,
|
||||
args: am,
|
||||
skipped: s.skipped,
|
||||
qc: s.qc,
|
||||
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
|
||||
}
|
||||
|
||||
func prepareRoleStmt(ctx context.Context, tx pgx.Tx) error {
|
||||
func prepare(c context.Context, tx pgx.Tx, st *stmt, key string) error {
|
||||
finalSQL, am := processTemplate(st.sql)
|
||||
|
||||
sd, err := tx.Prepare(c, "", finalSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_preparedList[key] = &preparedItem{
|
||||
sd: sd,
|
||||
args: am,
|
||||
st: st,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
func prepareRoleStmt(c context.Context, tx pgx.Tx) error {
|
||||
if len(conf.RolesQuery) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -128,7 +167,7 @@ func prepareRoleStmt(ctx context.Context, tx pgx.Tx) error {
|
||||
|
||||
roleSQL, _ := processTemplate(w.String())
|
||||
|
||||
_, err := tx.Prepare(ctx, "_sg_get_role", roleSQL)
|
||||
_, err := tx.Prepare(c, "_sg_get_role", roleSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
|
||||
// Ensure that we use the correct events, as they are not uniform across
|
||||
// platforms. See https://github.com/fsnotify/fsnotify/issues/74
|
||||
|
||||
if conf.Production == false && strings.HasSuffix(event.Name, "/allow.list") {
|
||||
if !conf.Production && strings.HasSuffix(event.Name, "/allow.list") {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -168,7 +168,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error {
|
||||
func ReExec() {
|
||||
err := syscall.Exec(binSelf, append([]string{binSelf}, os.Args[1:]...), os.Environ())
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("cannot restart")
|
||||
errlog.Fatal().Err(err).Msg("cannot restart")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ func buildFn(r configRemote) func(http.Header, []byte) ([]byte, error) {
|
||||
|
||||
res, err := client.Do(req)
|
||||
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
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
346
serv/rice-box.go
Normal file
346
serv/rice-box.go
Normal file
File diff suppressed because one or more lines are too long
36
serv/serv.go
36
serv/serv.go
@ -15,13 +15,15 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
conf := qcode.Config{
|
||||
Blocklist: c.DB.Defaults.Blocklist,
|
||||
Blocklist: c.DB.Blocklist,
|
||||
KeepArgs: false,
|
||||
}
|
||||
|
||||
@ -74,12 +76,16 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
||||
delete.Filters = blockFilter
|
||||
}
|
||||
|
||||
qc.AddRole(r.Name, t.Name, qcode.TRConfig{
|
||||
err := qc.AddRole(r.Name, t.Name, qcode.TRConfig{
|
||||
Query: query,
|
||||
Insert: insert,
|
||||
Update: update,
|
||||
Delete: delete,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +98,7 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
||||
}
|
||||
|
||||
func initWatcher(cpath string) {
|
||||
if conf.WatchAndReload == false {
|
||||
if !conf.WatchAndReload {
|
||||
return
|
||||
}
|
||||
|
||||
@ -106,7 +112,7 @@ func initWatcher(cpath string) {
|
||||
go func() {
|
||||
err := Do(logger.Printf, d)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Send()
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -139,7 +145,7 @@ func startHTTP() {
|
||||
<-sigint
|
||||
|
||||
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)
|
||||
}()
|
||||
@ -148,18 +154,16 @@ func startHTTP() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
var ident string
|
||||
|
||||
if len(conf.AppName) == 0 {
|
||||
ident = conf.Env
|
||||
} else {
|
||||
ident = conf.AppName
|
||||
}
|
||||
|
||||
fmt.Printf("%s listening on %s (%s)\n", serverName, hostPort, ident)
|
||||
logger.Info().
|
||||
Str("version", version).
|
||||
Str("git_branch", gitBranch).
|
||||
Str("host_post", hostPort).
|
||||
Str("app_name", conf.AppName).
|
||||
Str("env", conf.Env).
|
||||
Msgf("%s listening", serverName)
|
||||
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
logger.Error().Err(err).Msg("server closed")
|
||||
errlog.Error().Err(err).Msg("server closed")
|
||||
}
|
||||
|
||||
<-idleConnsClosed
|
||||
|
@ -28,9 +28,7 @@ func (pl *Logger) Log(ctx context.Context, level pgx.LogLevel, msg string, data
|
||||
zlevel = zerolog.ErrorLevel
|
||||
case pgx.LogLevelWarn:
|
||||
zlevel = zerolog.WarnLevel
|
||||
case pgx.LogLevelInfo:
|
||||
zlevel = zerolog.InfoLevel
|
||||
case pgx.LogLevelDebug:
|
||||
case pgx.LogLevelDebug, pgx.LogLevelInfo:
|
||||
zlevel = zerolog.DebugLevel
|
||||
default:
|
||||
zlevel = zerolog.DebugLevel
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
)
|
||||
|
||||
// nolint: errcheck
|
||||
func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 {
|
||||
h.WriteString(k1)
|
||||
h.WriteString(k2)
|
||||
@ -21,6 +22,7 @@ func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 {
|
||||
return v
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
func gqlHash(b string, vars []byte, role string) string {
|
||||
b = strings.TrimSpace(b)
|
||||
h := sha1.New()
|
||||
@ -64,7 +66,7 @@ func gqlHash(b string, vars []byte, role string) string {
|
||||
} else {
|
||||
starting = false
|
||||
s = e
|
||||
for e < len(b) && ws(b[e]) == false {
|
||||
for e < len(b) && !ws(b[e]) {
|
||||
e++
|
||||
}
|
||||
if e != 0 {
|
||||
@ -81,7 +83,7 @@ func gqlHash(b string, vars []byte, role string) string {
|
||||
io.WriteString(h, role)
|
||||
}
|
||||
|
||||
if vars == nil || len(vars) == 0 {
|
||||
if len(vars) == 0 {
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
@ -106,17 +108,28 @@ func al(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||
}
|
||||
|
||||
func isMutation(sql string) bool {
|
||||
for i := range sql {
|
||||
b := sql[i]
|
||||
if b == '{' {
|
||||
return false
|
||||
}
|
||||
if al(b) {
|
||||
return (b == 'm' || b == 'M')
|
||||
func gqlName(b string) string {
|
||||
state, s := 0, 0
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
switch {
|
||||
case state == 2 && b[i] == '{':
|
||||
return b[s:i]
|
||||
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 {
|
||||
|
@ -229,3 +229,80 @@ func TestGQLHashWithVars2(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
20
tmpl/dev.yml
20
tmpl/dev.yml
@ -101,18 +101,14 @@ database:
|
||||
variables:
|
||||
admin_account_id: "5"
|
||||
|
||||
# Define defaults to for the field key and values below
|
||||
defaults:
|
||||
# filters: ["{ user_id: { eq: $user_id } }"]
|
||||
|
||||
# Field and table names that you wish to block
|
||||
blocklist:
|
||||
- ar_internal_metadata
|
||||
- schema_migrations
|
||||
- secret
|
||||
- password
|
||||
- encrypted
|
||||
- token
|
||||
# Field and table names that you wish to block
|
||||
blocklist:
|
||||
- ar_internal_metadata
|
||||
- schema_migrations
|
||||
- secret
|
||||
- password
|
||||
- encrypted
|
||||
- token
|
||||
|
||||
tables:
|
||||
- name: customers
|
||||
|
@ -7,7 +7,7 @@ host_port: 0.0.0.0:8080
|
||||
web_ui: false
|
||||
|
||||
# debug, info, warn, error, fatal, panic, disable
|
||||
log_level: "info"
|
||||
log_level: "warn"
|
||||
# When production mode is 'true' only queries
|
||||
# from the allow list are permitted.
|
||||
# When it's 'false' all queries are saved to the
|
||||
|
Reference in New Issue
Block a user