Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6691de1b7 | |||
e6934cda02 | |||
4cf7956ff5 | |||
5356455904 | |||
074aded5c0 | |||
c7557f761f | |||
09d6460a13 | |||
40c99e9ef3 | |||
75ff5510d4 | |||
1370d24985 | |||
ef50c1957b | |||
41ea6ef6f5 | |||
a266517d17 | |||
7831d27345 | |||
e102da839e | |||
68a378c00f | |||
d96eaf14f4 | |||
01e488b69d | |||
7a450b16ba | |||
1ad8cbf15b | |||
f69f1c67d5 | |||
a172193955 | |||
81338b6123 | |||
265b93b203 | |||
6c240e21b4 | |||
7930719eaa | |||
cc687b1b2b | |||
3033dcf1a9 |
3
.gitignore
vendored
@ -24,16 +24,15 @@
|
||||
/demo/tmp
|
||||
|
||||
.vscode
|
||||
main
|
||||
.DS_Store
|
||||
.swp
|
||||
.release
|
||||
main
|
||||
super-graph
|
||||
supergraph
|
||||
*-fuzz.zip
|
||||
crashers
|
||||
suppressions
|
||||
release
|
||||
.gofuzz
|
||||
*-fuzz.zip
|
||||
|
||||
|
16
Dockerfile
@ -1,10 +1,12 @@
|
||||
# stage: 1
|
||||
FROM node:10 as react-build
|
||||
WORKDIR /web
|
||||
COPY web/ ./
|
||||
COPY /internal/serv/web/ ./
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
|
||||
|
||||
|
||||
# stage: 2
|
||||
FROM golang:1.14-alpine as go-build
|
||||
RUN apk update && \
|
||||
@ -22,8 +24,8 @@ RUN chmod 755 /usr/local/bin/sops
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
|
||||
RUN mkdir -p /app/web/build
|
||||
COPY --from=react-build /web/build/ ./web/build/
|
||||
RUN mkdir -p /app/internal/serv/web/build
|
||||
COPY --from=react-build /web/build/ ./internal/serv/web/build
|
||||
|
||||
RUN go mod vendor
|
||||
RUN make build
|
||||
@ -31,6 +33,8 @@ RUN echo "Compressing binary, will take a bit of time..." && \
|
||||
upx --ultra-brute -qq super-graph && \
|
||||
upx -t super-graph
|
||||
|
||||
|
||||
|
||||
# stage: 3
|
||||
FROM alpine:latest
|
||||
WORKDIR /
|
||||
@ -41,7 +45,7 @@ RUN mkdir -p /config
|
||||
COPY --from=go-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=go-build /app/config/* /config/
|
||||
COPY --from=go-build /app/super-graph .
|
||||
COPY --from=go-build /app/scripts/start.sh .
|
||||
COPY --from=go-build /app/internal/scripts/start.sh .
|
||||
COPY --from=go-build /usr/local/bin/sops .
|
||||
|
||||
RUN chmod +x /super-graph
|
||||
@ -49,7 +53,7 @@ RUN chmod +x /start.sh
|
||||
|
||||
USER nobody
|
||||
|
||||
EXPOSE 8080
|
||||
ENV GO_ENV production
|
||||
|
||||
CMD ./super-graph serv
|
||||
ENTRYPOINT ["./start.sh"]
|
||||
CMD ["./super-graph", "serv"]
|
||||
|
25
Makefile
@ -12,10 +12,10 @@ 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
|
||||
version = github.com/dosco/super-graph/internal/serv.version
|
||||
gitBranch = github.com/dosco/super-graph/internal/serv.gitBranch
|
||||
lastCommitSHA = github.com/dosco/super-graph/internal/serv.lastCommitSHA
|
||||
lastCommitTime = github.com/dosco/super-graph/internal/serv.lastCommitTime
|
||||
|
||||
BUILD_FLAGS ?= -ldflags '-s -w -X ${lastCommitSHA}=${BUILD} -X "${lastCommitTime}=${BUILD_DATE}" -X "${version}=${BUILD_VERSION}" -X ${gitBranch}=${BUILD_BRANCH}'
|
||||
|
||||
@ -28,18 +28,18 @@ BIN_DIR := $(GOPATH)/bin
|
||||
GORICE := $(BIN_DIR)/rice
|
||||
GOLANGCILINT := $(BIN_DIR)/golangci-lint
|
||||
GITCHGLOG := $(BIN_DIR)/git-chglog
|
||||
WEB_BUILD_DIR := ./web/build/manifest.json
|
||||
WEB_BUILD_DIR := ./internal/serv/web/build/manifest.json
|
||||
|
||||
$(GORICE):
|
||||
@GO111MODULE=off go get -u github.com/GeertJohan/go.rice/rice
|
||||
|
||||
$(WEB_BUILD_DIR):
|
||||
@echo "First install Yarn and create a build of the web UI found under ./web"
|
||||
@echo "Command: cd web && yarn && yarn build"
|
||||
@echo "First install Yarn and create a build of the web UI then re-run make install"
|
||||
@echo "Run this command: yarn --cwd internal/serv/web/ build"
|
||||
@exit 1
|
||||
|
||||
$(GITCHGLOG):
|
||||
@GO111MODULE=off go get -u github.com/git-chglog/git-chglog/cmd/git-chglog
|
||||
@GO111MODULE=off go get -u github.com/git-chglog/git-chglog/git-chglog
|
||||
|
||||
changelog: $(GITCHGLOG)
|
||||
@git-chglog $(ARGS)
|
||||
@ -57,7 +57,7 @@ os = $(word 1, $@)
|
||||
|
||||
$(PLATFORMS): lint test
|
||||
@mkdir -p release
|
||||
@GOOS=$(os) GOARCH=amd64 go build $(BUILD_FLAGS) -o release/$(BINARY)-$(BUILD_VERSION)-$(os)-amd64
|
||||
@GOOS=$(os) GOARCH=amd64 go build $(BUILD_FLAGS) -o release/$(BINARY)-$(BUILD_VERSION)-$(os)-amd64 main.go
|
||||
|
||||
release: windows linux darwin
|
||||
|
||||
@ -69,7 +69,7 @@ gen: $(GORICE) $(WEB_BUILD_DIR)
|
||||
@go generate ./...
|
||||
|
||||
$(BINARY): clean
|
||||
@go build $(BUILD_FLAGS) -o $(BINARY)
|
||||
@go build $(BUILD_FLAGS) -o $(BINARY) main.go
|
||||
|
||||
clean:
|
||||
@rm -f $(BINARY)
|
||||
@ -77,11 +77,10 @@ clean:
|
||||
run: clean
|
||||
@go run $(BUILD_FLAGS) main.go $(ARGS)
|
||||
|
||||
install: gen
|
||||
@echo $(GOPATH)
|
||||
install: clean build
|
||||
@echo "Commit Hash: `git rev-parse HEAD`"
|
||||
@echo "Old Hash: `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`"
|
||||
@go install $(BUILD_FLAGS)
|
||||
@mv $(BINARY) $(GOPATH)/bin/$(BINARY)
|
||||
@echo "New Hash:" `shasum $(GOPATH)/bin/$(BINARY) 2>/dev/null | cut -c -32`
|
||||
|
||||
uninstall: clean
|
||||
|
79
README.md
@ -1,26 +1,71 @@
|
||||
<!-- <a href="https://supergraph.dev"><img src="https://supergraph.dev/hologram.svg" width="100" height="100" align="right" /></a> -->
|
||||
|
||||
<img src="docs/.vuepress/public/super-graph.png" width="250" />
|
||||
<img src="docs/guide/.vuepress/public/super-graph.png" width="250" />
|
||||
|
||||
### Build web products faster. Secure high performance GraphQL
|
||||
|
||||

|
||||

|
||||

|
||||
[](https://pkg.go.dev/github.com/dosco/super-graph/core?tab=doc)
|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/6pSWCTZ)
|
||||
|
||||
## What's Super Graph?
|
||||
|
||||
## What is Super Graph
|
||||
Designed to 100x your developer productivity. Super Graph will instantly and without you writing code provide you a high performance GraphQL API for Postgres DB. GraphQL queries are compiled into a single fast SQL query. Super Graph is a GO library and a service, use it in your own code or run it as a seperate service.
|
||||
|
||||
Is designed to 100x your developer productivity. Super Graph will instantly and without you writing code provide you a high performance and secure GraphQL API for Postgres DB. GraphQL queries are 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.
|
||||
## Using it as a service
|
||||
|
||||
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, support for JWT tokens, built-in DB mutations and seeding, and a lot more.
|
||||
```console
|
||||
get get https://github.com/dosco/super-graph
|
||||
super-graph new <app_name>
|
||||
```
|
||||
|
||||

|
||||
## Using it in your own code
|
||||
|
||||
```golang
|
||||
package main
|
||||
|
||||
## The story of Super Graph?
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/dosco/super-graph/core"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("pgx", "postgres://postgrs:@localhost:5432/example_db")
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
conf, err := core.ReadInConfig("./config/dev.yml")
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
sg, err = core.NewSuperGraph(conf, db)
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
query := `
|
||||
query {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
}
|
||||
}`
|
||||
|
||||
res, err := sg.GraphQL(context.Background(), query, nil)
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
fmt.Println(string(res.Data))
|
||||
}
|
||||
```
|
||||
|
||||
## About 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.
|
||||
|
||||
@ -37,6 +82,7 @@ This compiler is what sits at the heart of Super Graph with layers of useful fun
|
||||
- Complex nested queries and mutations
|
||||
- Auto learns database tables and relationships
|
||||
- Role and Attribute based access control
|
||||
- Opaque cursor based efficient pagination
|
||||
- Full text search and aggregations
|
||||
- JWT tokens supported (Auth0, etc)
|
||||
- Join database queries with remote REST APIs
|
||||
@ -50,15 +96,6 @@ This compiler is what sits at the heart of Super Graph with layers of useful fun
|
||||
- Database seeding tool
|
||||
- Works with Postgres and YugabyteDB
|
||||
|
||||
## Get started
|
||||
|
||||
```
|
||||
git clone https://github.com/dosco/super-graph
|
||||
cd ./super-graph
|
||||
make install
|
||||
|
||||
super-graph new <app_name>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
|
@ -2,7 +2,7 @@ app_name: "Super Graph Development"
|
||||
host_port: 0.0.0.0:8080
|
||||
web_ui: true
|
||||
|
||||
# debug, info, warn, error, fatal, panic
|
||||
# debug, error, warn, info, none
|
||||
log_level: "debug"
|
||||
|
||||
# enable or disable http compression (uses gzip)
|
||||
@ -30,12 +30,21 @@ reload_on_config_change: true
|
||||
# seed_file: seed.js
|
||||
|
||||
# Path pointing to where the migrations can be found
|
||||
migrations_path: ./config/migrations
|
||||
migrations_path: ./migrations
|
||||
|
||||
# Secret key for general encryption operations like
|
||||
# encrypting the cursor data
|
||||
secret_key: supercalifajalistics
|
||||
|
||||
# CORS: A list of origins a cross-domain request can be executed from.
|
||||
# If the special * value is present in the list, all origins will be allowed.
|
||||
# An origin may contain a wildcard (*) to replace 0 or more
|
||||
# characters (i.e.: http://*.domain.com).
|
||||
cors_allowed_origins: ["*"]
|
||||
|
||||
# Debug Cross Origin Resource Sharing requests
|
||||
cors_debug: true
|
||||
|
||||
# Postgres related environment Variables
|
||||
# SG_DATABASE_HOST
|
||||
# SG_DATABASE_PORT
|
||||
@ -107,18 +116,18 @@ database:
|
||||
# database ping timeout is used for db health checking
|
||||
ping_timeout: 1m
|
||||
|
||||
# Define additional variables here to be used with filters
|
||||
variables:
|
||||
admin_account_id: "5"
|
||||
# Define additional variables here to be used with filters
|
||||
variables:
|
||||
admin_account_id: "5"
|
||||
|
||||
# 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
|
||||
|
@ -6,7 +6,7 @@ app_name: "Super Graph Production"
|
||||
host_port: 0.0.0.0:8080
|
||||
web_ui: false
|
||||
|
||||
# debug, info, warn, error, fatal, panic, disable
|
||||
# debug, error, warn, info, none
|
||||
log_level: "info"
|
||||
|
||||
# enable or disable http compression (uses gzip)
|
||||
@ -30,7 +30,7 @@ enable_tracing: true
|
||||
# seed_file: seed.js
|
||||
|
||||
# Path pointing to where the migrations can be found
|
||||
# migrations_path: migrations
|
||||
# migrations_path: ./migrations
|
||||
|
||||
# Secret key for general encryption operations like
|
||||
# encrypting the cursor data
|
||||
|
180
core/api.go
Normal file
@ -0,0 +1,180 @@
|
||||
// Package core provides the primary API to include and use Super Graph with your own code.
|
||||
// For detailed documentation visit https://supergraph.dev
|
||||
//
|
||||
// Example usage:
|
||||
/*
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/dosco/super-graph/core"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("pgx", "postgres://postgrs:@localhost:5432/example_db")
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
conf, err := core.ReadInConfig("./config/dev.yml")
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
sg, err = core.NewSuperGraph(conf, db)
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
query := `
|
||||
query {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
}
|
||||
}`
|
||||
|
||||
res, err := sg.GraphQL(context.Background(), query, nil)
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
fmt.Println(string(res.Data))
|
||||
}
|
||||
*/
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
_log "log"
|
||||
"os"
|
||||
|
||||
"github.com/dosco/super-graph/core/internal/allow"
|
||||
"github.com/dosco/super-graph/core/internal/crypto"
|
||||
"github.com/dosco/super-graph/core/internal/psql"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
)
|
||||
|
||||
type contextkey int
|
||||
|
||||
// Constants to set values on the context passed to the NewSuperGraph function
|
||||
const (
|
||||
// Name of the authentication provider. Eg. google, github, etc
|
||||
UserIDProviderKey contextkey = iota
|
||||
|
||||
// User ID value for authenticated users
|
||||
UserIDKey
|
||||
|
||||
// User role if pre-defined
|
||||
UserRoleKey
|
||||
)
|
||||
|
||||
// SuperGraph struct is an instance of the Super Graph engine it holds all the required information like
|
||||
// datase schemas, relationships, etc that the GraphQL to SQL compiler would need to do it's job.
|
||||
type SuperGraph struct {
|
||||
conf *Config
|
||||
db *sql.DB
|
||||
log *_log.Logger
|
||||
schema *psql.DBSchema
|
||||
allowList *allow.List
|
||||
encKey [32]byte
|
||||
prepared map[string]*preparedItem
|
||||
roles map[string]*Role
|
||||
getRole *sql.Stmt
|
||||
rmap map[uint64]*resolvFn
|
||||
abacEnabled bool
|
||||
anonExists bool
|
||||
qc *qcode.Compiler
|
||||
pc *psql.Compiler
|
||||
}
|
||||
|
||||
// NewSuperGraph creates the SuperGraph struct, this involves querying the database to learn its
|
||||
// schemas and relationships
|
||||
func NewSuperGraph(conf *Config, db *sql.DB) (*SuperGraph, error) {
|
||||
sg := &SuperGraph{
|
||||
conf: conf,
|
||||
db: db,
|
||||
log: _log.New(os.Stdout, "", 0),
|
||||
}
|
||||
|
||||
if err := sg.initConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := sg.initCompilers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := sg.initAllowList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := sg.initPrepared(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := sg.initResolvers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(conf.SecretKey) != 0 {
|
||||
sk := sha256.Sum256([]byte(conf.SecretKey))
|
||||
conf.SecretKey = ""
|
||||
sg.encKey = sk
|
||||
} else {
|
||||
sg.encKey = crypto.NewEncryptionKey()
|
||||
}
|
||||
|
||||
return sg, nil
|
||||
}
|
||||
|
||||
// Result struct contains the output of the GraphQL function this includes resulting json from the
|
||||
// database query and any error information
|
||||
type Result struct {
|
||||
op qcode.QType
|
||||
name string
|
||||
sql string
|
||||
role string
|
||||
|
||||
Error string `json:"message,omitempty"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
Extensions *extensions `json:"extensions,omitempty"`
|
||||
}
|
||||
|
||||
// GraphQL function is called on the SuperGraph struct to convert the provided GraphQL query into an
|
||||
// SQL query and execute it on the database. In production mode prepared statements are directly used
|
||||
// and no query compiling takes places.
|
||||
//
|
||||
// In developer mode all names queries are saved into a file `allow.list` and in production mode only
|
||||
// queries from this file can be run.
|
||||
func (sg *SuperGraph) GraphQL(c context.Context, query string, vars json.RawMessage) (*Result, error) {
|
||||
ct := scontext{Context: c, sg: sg, query: query, vars: vars}
|
||||
|
||||
if len(vars) <= 2 {
|
||||
ct.vars = nil
|
||||
}
|
||||
|
||||
if keyExists(c, UserIDKey) {
|
||||
ct.role = "user"
|
||||
} else {
|
||||
ct.role = "anon"
|
||||
}
|
||||
|
||||
ct.res.op = qcode.GetQType(query)
|
||||
ct.res.name = allow.QueryName(query)
|
||||
|
||||
data, err := ct.execQuery()
|
||||
if err != nil {
|
||||
return &ct.res, err
|
||||
}
|
||||
|
||||
ct.res.Data = json.RawMessage(data)
|
||||
|
||||
return &ct.res, nil
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package serv
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -10,29 +9,29 @@ import (
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
)
|
||||
|
||||
func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int, error) {
|
||||
func (c *scontext) argMap() 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 {
|
||||
if v := c.Value(UserIDProviderKey); v != nil {
|
||||
return io.WriteString(w, v.(string))
|
||||
}
|
||||
return 0, argErr("user_id_provider")
|
||||
|
||||
case "user_id":
|
||||
if v := ctx.Value(userIDKey); v != nil {
|
||||
if v := c.Value(UserIDKey); v != nil {
|
||||
return io.WriteString(w, v.(string))
|
||||
}
|
||||
return 0, argErr("user_id")
|
||||
|
||||
case "user_role":
|
||||
if v := ctx.Value(userRoleKey); v != nil {
|
||||
if v := c.Value(UserRoleKey); v != nil {
|
||||
return io.WriteString(w, v.(string))
|
||||
}
|
||||
return 0, argErr("user_role")
|
||||
}
|
||||
|
||||
fields := jsn.Get(vars, [][]byte{[]byte(tag)})
|
||||
fields := jsn.Get(c.vars, [][]byte{[]byte(tag)})
|
||||
|
||||
if len(fields) == 0 {
|
||||
return 0, argErr(tag)
|
||||
@ -49,7 +48,7 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
|
||||
if bytes.EqualFold(v, []byte("null")) {
|
||||
return io.WriteString(w, ``)
|
||||
}
|
||||
v1, err := decrypt(string(fields[0].Value))
|
||||
v1, err := c.sg.decrypt(string(fields[0].Value))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -61,14 +60,14 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
|
||||
}
|
||||
}
|
||||
|
||||
func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
||||
func (c *scontext) argList(args [][]byte) ([]interface{}, error) {
|
||||
vars := make([]interface{}, len(args))
|
||||
|
||||
var fields map[string]json.RawMessage
|
||||
var err error
|
||||
|
||||
if len(ctx.req.Vars) != 0 {
|
||||
fields, _, err = jsn.Tree(ctx.req.Vars)
|
||||
if len(c.vars) != 0 {
|
||||
fields, _, err = jsn.Tree(c.vars)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -79,21 +78,21 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
||||
av := args[i]
|
||||
switch {
|
||||
case bytes.Equal(av, []byte("user_id")):
|
||||
if v := ctx.Value(userIDKey); v != nil {
|
||||
if v := c.Value(UserIDKey); v != nil {
|
||||
vars[i] = v.(string)
|
||||
} else {
|
||||
return nil, argErr("user_id")
|
||||
}
|
||||
|
||||
case bytes.Equal(av, []byte("user_id_provider")):
|
||||
if v := ctx.Value(userIDProviderKey); v != nil {
|
||||
if v := c.Value(UserIDProviderKey); v != nil {
|
||||
vars[i] = v.(string)
|
||||
} else {
|
||||
return nil, argErr("user_id_provider")
|
||||
}
|
||||
|
||||
case bytes.Equal(av, []byte("user_role")):
|
||||
if v := ctx.Value(userRoleKey); v != nil {
|
||||
if v := c.Value(UserRoleKey); v != nil {
|
||||
vars[i] = v.(string)
|
||||
} else {
|
||||
return nil, argErr("user_role")
|
||||
@ -101,7 +100,7 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
|
||||
|
||||
case bytes.Equal(av, []byte("cursor")):
|
||||
if v, ok := fields["cursor"]; ok && v[0] == '"' {
|
||||
v1, err := decrypt(string(v[1 : len(v)-1]))
|
||||
v1, err := c.sg.decrypt(string(v[1 : len(v)-1]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package serv
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -7,42 +7,42 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/dosco/super-graph/psql"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/core/internal/psql"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
)
|
||||
|
||||
type stmt struct {
|
||||
role *configRole
|
||||
role *Role
|
||||
qc *qcode.QCode
|
||||
skipped uint32
|
||||
sql string
|
||||
}
|
||||
|
||||
func buildStmt(qt qcode.QType, gql, vars []byte, role string) ([]stmt, error) {
|
||||
func (sg *SuperGraph) buildStmt(qt qcode.QType, query, vars []byte, role string) ([]stmt, error) {
|
||||
switch qt {
|
||||
case qcode.QTMutation:
|
||||
return buildRoleStmt(gql, vars, role)
|
||||
return sg.buildRoleStmt(query, vars, role)
|
||||
|
||||
case qcode.QTQuery:
|
||||
if role == "anon" {
|
||||
return buildRoleStmt(gql, vars, "anon")
|
||||
return sg.buildRoleStmt(query, vars, "anon")
|
||||
}
|
||||
|
||||
if conf.isABACEnabled() {
|
||||
return buildMultiStmt(gql, vars)
|
||||
if sg.abacEnabled {
|
||||
return sg.buildMultiStmt(query, vars)
|
||||
}
|
||||
|
||||
return buildRoleStmt(gql, vars, "user")
|
||||
return sg.buildRoleStmt(query, vars, "user")
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown query type '%d'", qt)
|
||||
}
|
||||
}
|
||||
|
||||
func buildRoleStmt(gql, vars []byte, role string) ([]stmt, error) {
|
||||
ro, ok := conf.roles[role]
|
||||
func (sg *SuperGraph) buildRoleStmt(query, vars []byte, role string) ([]stmt, error) {
|
||||
ro, ok := sg.roles[role]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`roles '%s' not defined in config`, role)
|
||||
return nil, fmt.Errorf(`roles '%s' not defined in c.sg.config`, role)
|
||||
}
|
||||
|
||||
var vm map[string]json.RawMessage
|
||||
@ -54,21 +54,15 @@ func buildRoleStmt(gql, vars []byte, role string) ([]stmt, error) {
|
||||
}
|
||||
}
|
||||
|
||||
qc, err := qcompile.Compile(gql, ro.Name)
|
||||
qc, err := sg.qc.Compile(query, ro.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For the 'anon' role in production only compile
|
||||
// queries for tables defined in the config file.
|
||||
if conf.Production && ro.Name == "anon" && !hasTablesWithConfig(qc, ro) {
|
||||
return nil, errors.New("query contains tables with no 'anon' role config")
|
||||
}
|
||||
|
||||
stmts := []stmt{stmt{role: ro, qc: qc}}
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
skipped, err := pcompile.Compile(qc, w, psql.Variables(vm))
|
||||
skipped, err := sg.pc.Compile(qc, w, psql.Variables(vm))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -79,7 +73,7 @@ func buildRoleStmt(gql, vars []byte, role string) ([]stmt, error) {
|
||||
return stmts, nil
|
||||
}
|
||||
|
||||
func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
|
||||
func (sg *SuperGraph) buildMultiStmt(query, vars []byte) ([]stmt, error) {
|
||||
var vm map[string]json.RawMessage
|
||||
var err error
|
||||
|
||||
@ -89,28 +83,29 @@ func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(conf.RolesQuery) == 0 {
|
||||
return buildRoleStmt(gql, vars, "user")
|
||||
if len(sg.conf.RolesQuery) == 0 {
|
||||
return nil, errors.New("roles_query not defined")
|
||||
}
|
||||
|
||||
stmts := make([]stmt, 0, len(conf.Roles))
|
||||
stmts := make([]stmt, 0, len(sg.conf.Roles))
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
for i := 0; i < len(conf.Roles); i++ {
|
||||
role := &conf.Roles[i]
|
||||
for i := 0; i < len(sg.conf.Roles); i++ {
|
||||
role := &sg.conf.Roles[i]
|
||||
|
||||
// skip anon as it's not included in the combined multi-statement
|
||||
if role.Name == "anon" {
|
||||
continue
|
||||
}
|
||||
|
||||
qc, err := qcompile.Compile(gql, role.Name)
|
||||
qc, err := sg.qc.Compile(query, role.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmts = append(stmts, stmt{role: role, qc: qc})
|
||||
|
||||
skipped, err := pcompile.Compile(qc, w, psql.Variables(vm))
|
||||
skipped, err := sg.pc.Compile(qc, w, psql.Variables(vm))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -121,7 +116,7 @@ func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
|
||||
w.Reset()
|
||||
}
|
||||
|
||||
sql, err := renderUserQuery(stmts, vm)
|
||||
sql, err := sg.renderUserQuery(stmts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -131,8 +126,7 @@ func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
|
||||
}
|
||||
|
||||
//nolint: errcheck
|
||||
func renderUserQuery(
|
||||
stmts []stmt, vars map[string]json.RawMessage) (string, error) {
|
||||
func (sg *SuperGraph) renderUserQuery(stmts []stmt) (string, error) {
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
io.WriteString(w, `SELECT "_sg_auth_info"."role", (CASE "_sg_auth_info"."role" `)
|
||||
@ -150,7 +144,7 @@ func renderUserQuery(
|
||||
}
|
||||
|
||||
io.WriteString(w, `END) FROM (SELECT (CASE WHEN EXISTS (`)
|
||||
io.WriteString(w, conf.RolesQuery)
|
||||
io.WriteString(w, sg.conf.RolesQuery)
|
||||
io.WriteString(w, `) THEN `)
|
||||
|
||||
io.WriteString(w, `(SELECT (CASE`)
|
||||
@ -166,20 +160,21 @@ func renderUserQuery(
|
||||
}
|
||||
|
||||
io.WriteString(w, ` ELSE 'user' END) FROM (`)
|
||||
io.WriteString(w, conf.RolesQuery)
|
||||
io.WriteString(w, sg.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 w.String(), nil
|
||||
}
|
||||
|
||||
func hasTablesWithConfig(qc *qcode.QCode, role *configRole) bool {
|
||||
func (sg *SuperGraph) hasTablesWithConfig(qc *qcode.QCode, role *Role) bool {
|
||||
for _, id := range qc.Roots {
|
||||
t, err := schema.GetTable(qc.Selects[id].Name)
|
||||
t, err := sg.schema.GetTable(qc.Selects[id].Name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if _, ok := role.tablesMap[t.Name]; !ok {
|
||||
|
||||
if r := role.GetTable(t.Name); r == nil {
|
||||
return false
|
||||
}
|
||||
}
|
163
core/config.go
Normal file
@ -0,0 +1,163 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Core struct contains core specific config value
|
||||
type Config struct {
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
UseAllowList bool `mapstructure:"use_allow_list"`
|
||||
AllowListFile string `mapstructure:"allow_list_file"`
|
||||
SetUserID bool `mapstructure:"set_user_id"`
|
||||
Vars map[string]string `mapstructure:"variables"`
|
||||
Blocklist []string
|
||||
Tables []Table
|
||||
RolesQuery string `mapstructure:"roles_query"`
|
||||
Roles []Role
|
||||
Inflections map[string]string
|
||||
}
|
||||
|
||||
// Table struct defines a database table
|
||||
type Table struct {
|
||||
Name string
|
||||
Table string
|
||||
Blocklist []string
|
||||
Remotes []Remote
|
||||
Columns []Column
|
||||
}
|
||||
|
||||
// Column struct defines a database column
|
||||
type Column struct {
|
||||
Name string
|
||||
Type string
|
||||
ForeignKey string `mapstructure:"related_to"`
|
||||
}
|
||||
|
||||
// Remote struct defines a remote API endpoint
|
||||
type Remote struct {
|
||||
Name string
|
||||
ID string
|
||||
Path string
|
||||
URL string
|
||||
Debug bool
|
||||
PassHeaders []string `mapstructure:"pass_headers"`
|
||||
SetHeaders []struct {
|
||||
Name string
|
||||
Value string
|
||||
} `mapstructure:"set_headers"`
|
||||
}
|
||||
|
||||
// Role struct contains role specific access control values for for all database tables
|
||||
type Role struct {
|
||||
Name string
|
||||
Match string
|
||||
Tables []RoleTable
|
||||
tm map[string]*RoleTable
|
||||
}
|
||||
|
||||
// RoleTable struct contains role specific access control values for a database table
|
||||
type RoleTable struct {
|
||||
Name string
|
||||
|
||||
Query Query
|
||||
Insert Insert
|
||||
Update Update
|
||||
Delete Delete
|
||||
}
|
||||
|
||||
// Query struct contains access control values for query operations
|
||||
type Query struct {
|
||||
Limit int
|
||||
Filters []string
|
||||
Columns []string
|
||||
DisableFunctions bool `mapstructure:"disable_functions"`
|
||||
Block bool
|
||||
}
|
||||
|
||||
// Insert struct contains access control values for insert operations
|
||||
type Insert struct {
|
||||
Filters []string
|
||||
Columns []string
|
||||
Presets map[string]string
|
||||
Block bool
|
||||
}
|
||||
|
||||
// Insert struct contains access control values for update operations
|
||||
type Update struct {
|
||||
Filters []string
|
||||
Columns []string
|
||||
Presets map[string]string
|
||||
Block bool
|
||||
}
|
||||
|
||||
// Delete struct contains access control values for delete operations
|
||||
type Delete struct {
|
||||
Filters []string
|
||||
Columns []string
|
||||
Block bool
|
||||
}
|
||||
|
||||
// ReadInConfig function reads in the config file for the environment specified in the GO_ENV
|
||||
// environment variable. This is the best way to create a new Super Graph config.
|
||||
func ReadInConfig(configFile string) (*Config, error) {
|
||||
cpath := path.Dir(configFile)
|
||||
cfile := path.Base(configFile)
|
||||
vi := newViper(cpath, cfile)
|
||||
|
||||
if err := vi.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inherits := vi.GetString("inherits")
|
||||
|
||||
if len(inherits) != 0 {
|
||||
vi = newViper(cpath, inherits)
|
||||
|
||||
if err := vi.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if vi.IsSet("inherits") {
|
||||
return nil, fmt.Errorf("inherited config (%s) cannot itself inherit (%s)",
|
||||
inherits,
|
||||
vi.GetString("inherits"))
|
||||
}
|
||||
|
||||
vi.SetConfigName(cfile)
|
||||
|
||||
if err := vi.MergeInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c := &Config{}
|
||||
|
||||
if err := vi.Unmarshal(&c); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode config, %v", err)
|
||||
}
|
||||
|
||||
if len(c.AllowListFile) == 0 {
|
||||
c.AllowListFile = path.Join(cpath, "allow.list")
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func newViper(configPath, configFile string) *viper.Viper {
|
||||
vi := viper.New()
|
||||
|
||||
vi.SetEnvPrefix("SG")
|
||||
vi.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
vi.AutomaticEnv()
|
||||
|
||||
vi.SetConfigName(configFile)
|
||||
vi.AddConfigPath(configPath)
|
||||
vi.AddConfigPath("./config")
|
||||
|
||||
return vi
|
||||
}
|
19
core/consts.go
Normal file
@ -0,0 +1,19 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
const (
|
||||
openVar = "{{"
|
||||
closeVar = "}}"
|
||||
)
|
||||
|
||||
var (
|
||||
errNotFound = errors.New("not found in prepared statements")
|
||||
)
|
||||
|
||||
func keyExists(ct context.Context, key contextkey) bool {
|
||||
return ct.Value(key) != nil
|
||||
}
|
395
core/core.go
Normal file
@ -0,0 +1,395 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dosco/super-graph/core/internal/psql"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
|
||||
"github.com/valyala/fasttemplate"
|
||||
)
|
||||
|
||||
type extensions struct {
|
||||
Tracing *trace `json:"tracing,omitempty"`
|
||||
}
|
||||
|
||||
type trace struct {
|
||||
Version int `json:"version"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Execution execution `json:"execution"`
|
||||
}
|
||||
|
||||
type execution struct {
|
||||
Resolvers []resolver `json:"resolvers"`
|
||||
}
|
||||
|
||||
type resolver struct {
|
||||
Path []string `json:"path"`
|
||||
ParentType string `json:"parentType"`
|
||||
FieldName string `json:"fieldName"`
|
||||
ReturnType string `json:"returnType"`
|
||||
StartOffset int `json:"startOffset"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
}
|
||||
|
||||
type scontext struct {
|
||||
context.Context
|
||||
|
||||
sg *SuperGraph
|
||||
query string
|
||||
vars json.RawMessage
|
||||
role string
|
||||
res Result
|
||||
}
|
||||
|
||||
func (sg *SuperGraph) initCompilers() error {
|
||||
di, err := psql.GetDBInfo(sg.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = addTables(sg.conf, di); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = addForeignKeys(sg.conf, di); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sg.schema, err = psql.NewDBSchema(di, getDBTableAliases(sg.conf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sg.qc, err = qcode.NewCompiler(qcode.Config{
|
||||
Blocklist: sg.conf.Blocklist,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := addRoles(sg.conf, sg.qc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sg.pc = psql.NewCompiler(psql.Config{
|
||||
Schema: sg.schema,
|
||||
Vars: sg.conf.Vars,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *scontext) execQuery() ([]byte, error) {
|
||||
var data []byte
|
||||
var st *stmt
|
||||
var err error
|
||||
|
||||
if c.sg.conf.UseAllowList {
|
||||
data, st, err = c.resolvePreparedSQL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else {
|
||||
data, st, err = c.resolveSQL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) == 0 || st.skipped == 0 {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// return c.sg.execRemoteJoin(st, data, c.req.hdr)
|
||||
return c.sg.execRemoteJoin(st, data, nil)
|
||||
}
|
||||
|
||||
func (c *scontext) resolvePreparedSQL() ([]byte, *stmt, error) {
|
||||
var tx *sql.Tx
|
||||
var err error
|
||||
|
||||
mutation := (c.res.op == qcode.QTMutation)
|
||||
useRoleQuery := c.sg.abacEnabled && mutation
|
||||
useTx := useRoleQuery || c.sg.conf.SetUserID
|
||||
|
||||
if useTx {
|
||||
if tx, err = c.sg.db.BeginTx(c, nil); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer tx.Rollback() //nolint: errcheck
|
||||
}
|
||||
|
||||
if c.sg.conf.SetUserID {
|
||||
if err := setLocalUserID(c, tx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var role string
|
||||
|
||||
if useRoleQuery {
|
||||
if role, err = c.executeRoleQuery(tx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
} else if v := c.Value(UserRoleKey); v != nil {
|
||||
role = v.(string)
|
||||
|
||||
} else {
|
||||
role = c.role
|
||||
|
||||
}
|
||||
|
||||
c.res.role = role
|
||||
|
||||
ps, ok := c.sg.prepared[stmtHash(c.res.name, role)]
|
||||
if !ok {
|
||||
return nil, nil, errNotFound
|
||||
}
|
||||
c.res.sql = ps.st.sql
|
||||
|
||||
var root []byte
|
||||
var row *sql.Row
|
||||
|
||||
varsList, err := c.argList(ps.args)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if useTx {
|
||||
row = tx.Stmt(ps.sd).QueryRow(varsList...)
|
||||
} else {
|
||||
row = ps.sd.QueryRow(varsList...)
|
||||
}
|
||||
|
||||
if ps.roleArg {
|
||||
err = row.Scan(&role, &root)
|
||||
} else {
|
||||
err = row.Scan(&root)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
c.role = role
|
||||
|
||||
if useTx {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if root, err = c.sg.encryptCursor(ps.st.qc, root); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return root, &ps.st, nil
|
||||
}
|
||||
|
||||
func (c *scontext) resolveSQL() ([]byte, *stmt, error) {
|
||||
var tx *sql.Tx
|
||||
var err error
|
||||
|
||||
mutation := (c.res.op == qcode.QTMutation)
|
||||
useRoleQuery := c.sg.abacEnabled && mutation
|
||||
useTx := useRoleQuery || c.sg.conf.SetUserID
|
||||
|
||||
if useTx {
|
||||
if tx, err = c.sg.db.BeginTx(c, nil); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer tx.Rollback() //nolint: errcheck
|
||||
}
|
||||
|
||||
if c.sg.conf.SetUserID {
|
||||
if err := setLocalUserID(c, tx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if useRoleQuery {
|
||||
if c.role, err = c.executeRoleQuery(tx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
} else if v := c.Value(UserRoleKey); v != nil {
|
||||
c.role = v.(string)
|
||||
}
|
||||
|
||||
stmts, err := c.sg.buildStmt(c.res.op, []byte(c.query), c.vars, c.role)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
st := &stmts[0]
|
||||
|
||||
t := fasttemplate.New(st.sql, openVar, closeVar)
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
_, err = t.ExecuteFunc(buf, c.argMap())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
finalSQL := buf.String()
|
||||
|
||||
// var stime time.Time
|
||||
|
||||
// if c.sg.conf.EnableTracing {
|
||||
// stime = time.Now()
|
||||
// }
|
||||
|
||||
var root []byte
|
||||
var role string
|
||||
var row *sql.Row
|
||||
|
||||
// defaultRole := c.role
|
||||
|
||||
if useTx {
|
||||
row = tx.QueryRow(finalSQL)
|
||||
} else {
|
||||
row = c.sg.db.QueryRow(finalSQL)
|
||||
}
|
||||
|
||||
if len(stmts) > 1 {
|
||||
err = row.Scan(&role, &root)
|
||||
} else {
|
||||
err = row.Scan(&root)
|
||||
}
|
||||
|
||||
c.res.sql = finalSQL
|
||||
|
||||
if len(role) == 0 {
|
||||
c.res.role = c.role
|
||||
} else {
|
||||
c.res.role = role
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if useTx {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if root, err = c.sg.encryptCursor(st.qc, root); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if c.sg.allowList.IsPersist() {
|
||||
if err := c.sg.allowList.Set(c.vars, c.query, ""); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(stmts) > 1 {
|
||||
if st = findStmt(role, stmts); st == nil {
|
||||
return nil, nil, fmt.Errorf("invalid role '%s' returned", role)
|
||||
}
|
||||
}
|
||||
|
||||
// if c.sg.conf.EnableTracing {
|
||||
// for _, id := range st.qc.Roots {
|
||||
// c.addTrace(st.qc.Selects, id, stime)
|
||||
// }
|
||||
// }
|
||||
|
||||
return root, st, nil
|
||||
}
|
||||
|
||||
func (c *scontext) executeRoleQuery(tx *sql.Tx) (string, error) {
|
||||
userID := c.Value(UserIDKey)
|
||||
|
||||
if userID == nil {
|
||||
return "anon", nil
|
||||
}
|
||||
|
||||
var role string
|
||||
row := c.sg.getRole.QueryRow(userID, c.role)
|
||||
|
||||
if err := row.Scan(&role); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (r *Result) Operation() string {
|
||||
return r.op.String()
|
||||
}
|
||||
|
||||
func (r *Result) QueryName() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
func (r *Result) Role() string {
|
||||
return r.role
|
||||
}
|
||||
|
||||
func (r *Result) SQL() string {
|
||||
return r.sql
|
||||
}
|
||||
|
||||
// func (c *scontext) addTrace(sel []qcode.Select, id int32, st time.Time) {
|
||||
// et := time.Now()
|
||||
// du := et.Sub(st)
|
||||
|
||||
// if c.res.Extensions == nil {
|
||||
// c.res.Extensions = &extensions{&trace{
|
||||
// Version: 1,
|
||||
// StartTime: st,
|
||||
// Execution: execution{},
|
||||
// }}
|
||||
// }
|
||||
|
||||
// c.res.Extensions.Tracing.EndTime = et
|
||||
// c.res.Extensions.Tracing.Duration = du
|
||||
|
||||
// n := 1
|
||||
// for i := id; i != -1; i = sel[i].ParentID {
|
||||
// n++
|
||||
// }
|
||||
// path := make([]string, n)
|
||||
|
||||
// n--
|
||||
// for i := id; ; i = sel[i].ParentID {
|
||||
// path[n] = sel[i].Name
|
||||
// if sel[i].ParentID == -1 {
|
||||
// break
|
||||
// }
|
||||
// n--
|
||||
// }
|
||||
|
||||
// tr := resolver{
|
||||
// Path: path,
|
||||
// ParentType: "Query",
|
||||
// FieldName: sel[id].Name,
|
||||
// ReturnType: "object",
|
||||
// StartOffset: 1,
|
||||
// Duration: du,
|
||||
// }
|
||||
|
||||
// c.res.Extensions.Tracing.Execution.Resolvers =
|
||||
// append(c.res.Extensions.Tracing.Execution.Resolvers, tr)
|
||||
// }
|
||||
|
||||
func findStmt(role string, stmts []stmt) *stmt {
|
||||
for i := range stmts {
|
||||
if stmts[i].role.Name != role {
|
||||
continue
|
||||
}
|
||||
return &stmts[i]
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package serv
|
||||
package core
|
||||
|
||||
/*
|
||||
|
@ -1,15 +1,15 @@
|
||||
package serv
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/dosco/super-graph/crypto"
|
||||
"github.com/dosco/super-graph/core/internal/crypto"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
)
|
||||
|
||||
func encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) {
|
||||
func (sg *SuperGraph) encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) {
|
||||
var keys [][]byte
|
||||
|
||||
for _, s := range qc.Selects {
|
||||
@ -39,7 +39,7 @@ func encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if len(f.Value) > 2 {
|
||||
v, err := crypto.Encrypt(f.Value[1:len(f.Value)-1], &internalKey)
|
||||
v, err := crypto.Encrypt(f.Value[1:len(f.Value)-1], &sg.encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -63,10 +63,10 @@ func encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func decrypt(data string) ([]byte, error) {
|
||||
func (sg *SuperGraph) decrypt(data string) ([]byte, error) {
|
||||
v, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crypto.Decrypt(v, &internalKey)
|
||||
return crypto.Decrypt(v, &sg.encKey)
|
||||
}
|
15
core/db.go
Normal file
@ -0,0 +1,15 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func setLocalUserID(c context.Context, tx *sql.Tx) error {
|
||||
var err error
|
||||
if v := c.Value(UserIDKey); v != nil {
|
||||
_, err = tx.Exec(`SET LOCAL "user.id" = ?`, v)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
285
core/init.go
Normal file
@ -0,0 +1,285 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/dosco/super-graph/core/internal/psql"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
"github.com/gobuffalo/flect"
|
||||
)
|
||||
|
||||
func (sg *SuperGraph) initConfig() error {
|
||||
c := sg.conf
|
||||
|
||||
for k, v := range c.Inflections {
|
||||
flect.AddPlural(k, v)
|
||||
}
|
||||
|
||||
// Variables: Validate and sanitize
|
||||
for k, v := range c.Vars {
|
||||
c.Vars[k] = sanitizeVars(v)
|
||||
}
|
||||
|
||||
// Tables: Validate and sanitize
|
||||
tm := make(map[string]struct{})
|
||||
|
||||
for i := 0; i < len(c.Tables); i++ {
|
||||
t := &c.Tables[i]
|
||||
t.Name = flect.Pluralize(strings.ToLower(t.Name))
|
||||
|
||||
if _, ok := tm[t.Name]; ok {
|
||||
sg.conf.Tables = append(c.Tables[:i], c.Tables[i+1:]...)
|
||||
sg.log.Printf("WRN duplicate table found: %s", t.Name)
|
||||
}
|
||||
tm[t.Name] = struct{}{}
|
||||
|
||||
t.Table = flect.Pluralize(strings.ToLower(t.Table))
|
||||
}
|
||||
|
||||
sg.roles = make(map[string]*Role)
|
||||
|
||||
for i := 0; i < len(c.Roles); i++ {
|
||||
role := &c.Roles[i]
|
||||
role.Name = sanitize(role.Name)
|
||||
|
||||
if _, ok := sg.roles[role.Name]; ok {
|
||||
c.Roles = append(c.Roles[:i], c.Roles[i+1:]...)
|
||||
sg.log.Printf("WRN duplicate role found: %s", role.Name)
|
||||
}
|
||||
|
||||
role.Match = sanitize(role.Match)
|
||||
role.tm = make(map[string]*RoleTable)
|
||||
|
||||
for n, table := range role.Tables {
|
||||
role.tm[table.Name] = &role.Tables[n]
|
||||
}
|
||||
|
||||
sg.roles[role.Name] = role
|
||||
}
|
||||
|
||||
// If user role not defined then create it
|
||||
if _, ok := sg.roles["user"]; !ok {
|
||||
ur := Role{
|
||||
Name: "user",
|
||||
tm: make(map[string]*RoleTable),
|
||||
}
|
||||
c.Roles = append(c.Roles, ur)
|
||||
sg.roles["user"] = &ur
|
||||
}
|
||||
|
||||
// Roles: validate and sanitize
|
||||
c.RolesQuery = sanitizeVars(c.RolesQuery)
|
||||
|
||||
if len(c.RolesQuery) == 0 {
|
||||
sg.log.Printf("WRN roles_query not defined: attribute based access control disabled")
|
||||
}
|
||||
|
||||
_, userExists := sg.roles["user"]
|
||||
_, sg.anonExists = sg.roles["anon"]
|
||||
|
||||
sg.abacEnabled = userExists && len(c.RolesQuery) != 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDBTableAliases(c *Config) map[string][]string {
|
||||
m := make(map[string][]string, len(c.Tables))
|
||||
|
||||
for i := range c.Tables {
|
||||
t := c.Tables[i]
|
||||
|
||||
if len(t.Table) == 0 || len(t.Columns) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
m[t.Table] = append(m[t.Table], t.Name)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func addTables(c *Config, di *psql.DBInfo) error {
|
||||
for _, t := range c.Tables {
|
||||
if len(t.Table) == 0 || len(t.Columns) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := addTable(di, t.Columns, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addTable(di *psql.DBInfo, cols []Column, t Table) error {
|
||||
bc, ok := di.GetColumn(t.Table, t.Name)
|
||||
if !ok {
|
||||
return fmt.Errorf(
|
||||
"Column '%s' not found on table '%s'",
|
||||
t.Name, t.Table)
|
||||
}
|
||||
|
||||
if bc.Type != "json" && bc.Type != "jsonb" {
|
||||
return fmt.Errorf(
|
||||
"Column '%s' in table '%s' is of type '%s'. Only JSON or JSONB is valid",
|
||||
t.Name, t.Table, bc.Type)
|
||||
}
|
||||
|
||||
table := psql.DBTable{
|
||||
Name: t.Name,
|
||||
Key: strings.ToLower(t.Name),
|
||||
Type: bc.Type,
|
||||
}
|
||||
|
||||
columns := make([]psql.DBColumn, 0, len(cols))
|
||||
|
||||
for i := range cols {
|
||||
c := cols[i]
|
||||
columns = append(columns, psql.DBColumn{
|
||||
Name: c.Name,
|
||||
Key: strings.ToLower(c.Name),
|
||||
Type: c.Type,
|
||||
})
|
||||
}
|
||||
|
||||
di.AddTable(table, columns)
|
||||
bc.FKeyTable = t.Name
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addForeignKeys(c *Config, di *psql.DBInfo) error {
|
||||
for _, t := range c.Tables {
|
||||
for _, c := range t.Columns {
|
||||
if len(c.ForeignKey) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := addForeignKey(di, c, t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addForeignKey(di *psql.DBInfo, c Column, t Table) error {
|
||||
c1, ok := di.GetColumn(t.Name, c.Name)
|
||||
if !ok {
|
||||
return fmt.Errorf(
|
||||
"Invalid table '%s' or column '%s' in Config",
|
||||
t.Name, c.Name)
|
||||
}
|
||||
|
||||
v := strings.SplitN(c.ForeignKey, ".", 2)
|
||||
if len(v) != 2 {
|
||||
return fmt.Errorf(
|
||||
"Invalid foreign_key in Config for table '%s' and column '%s",
|
||||
t.Name, c.Name)
|
||||
}
|
||||
|
||||
fkt, fkc := v[0], v[1]
|
||||
c2, ok := di.GetColumn(fkt, fkc)
|
||||
if !ok {
|
||||
return fmt.Errorf(
|
||||
"Invalid foreign_key in Config for table '%s' and column '%s",
|
||||
t.Name, c.Name)
|
||||
}
|
||||
|
||||
c1.FKeyTable = fkt
|
||||
c1.FKeyColID = []int16{c2.ID}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addRoles(c *Config, qc *qcode.Compiler) error {
|
||||
for _, r := range c.Roles {
|
||||
for _, t := range r.Tables {
|
||||
if err := addRole(qc, r, t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addRole(qc *qcode.Compiler, r Role, t RoleTable) error {
|
||||
blockFilter := []string{"false"}
|
||||
|
||||
query := qcode.QueryConfig{
|
||||
Limit: t.Query.Limit,
|
||||
Filters: t.Query.Filters,
|
||||
Columns: t.Query.Columns,
|
||||
DisableFunctions: t.Query.DisableFunctions,
|
||||
}
|
||||
|
||||
if t.Query.Block {
|
||||
query.Filters = blockFilter
|
||||
}
|
||||
|
||||
insert := qcode.InsertConfig{
|
||||
Filters: t.Insert.Filters,
|
||||
Columns: t.Insert.Columns,
|
||||
Presets: t.Insert.Presets,
|
||||
}
|
||||
|
||||
if t.Insert.Block {
|
||||
insert.Filters = blockFilter
|
||||
}
|
||||
|
||||
update := qcode.UpdateConfig{
|
||||
Filters: t.Update.Filters,
|
||||
Columns: t.Update.Columns,
|
||||
Presets: t.Update.Presets,
|
||||
}
|
||||
|
||||
if t.Update.Block {
|
||||
update.Filters = blockFilter
|
||||
}
|
||||
|
||||
delete := qcode.DeleteConfig{
|
||||
Filters: t.Delete.Filters,
|
||||
Columns: t.Delete.Columns,
|
||||
}
|
||||
|
||||
if t.Delete.Block {
|
||||
delete.Filters = blockFilter
|
||||
}
|
||||
|
||||
return qc.AddRole(r.Name, t.Name, qcode.TRConfig{
|
||||
Query: query,
|
||||
Insert: insert,
|
||||
Update: update,
|
||||
Delete: delete,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Role) GetTable(name string) *RoleTable {
|
||||
return r.tm[name]
|
||||
}
|
||||
|
||||
func sanitize(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
var (
|
||||
varRe1 = regexp.MustCompile(`(?mi)\$([a-zA-Z0-9_.]+)`)
|
||||
varRe2 = regexp.MustCompile(`\{\{([a-zA-Z0-9_.]+)\}\}`)
|
||||
)
|
||||
|
||||
func sanitizeVars(s string) string {
|
||||
s0 := varRe1.ReplaceAllString(s, `{{$1}}`)
|
||||
|
||||
s1 := strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return ' '
|
||||
}
|
||||
return r
|
||||
}, s0)
|
||||
|
||||
return varRe2.ReplaceAllStringFunc(s1, func(m string) string {
|
||||
return strings.ToLower(m)
|
||||
})
|
||||
}
|
@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@ -35,11 +34,11 @@ type Config struct {
|
||||
Persist bool
|
||||
}
|
||||
|
||||
func New(cpath string, conf Config) (*List, error) {
|
||||
func New(filename string, conf Config) (*List, error) {
|
||||
al := List{}
|
||||
|
||||
if len(cpath) != 0 {
|
||||
fp := path.Join(cpath, "allow.list")
|
||||
if len(filename) != 0 {
|
||||
fp := filename
|
||||
|
||||
if _, err := os.Stat(fp); err == nil {
|
||||
al.filepath = fp
|
||||
@ -73,10 +72,10 @@ func New(cpath string, conf Config) (*List, error) {
|
||||
return nil, errors.New("allow.list not found")
|
||||
}
|
||||
|
||||
if len(cpath) == 0 {
|
||||
if len(filename) == 0 {
|
||||
al.filepath = "./config/allow.list"
|
||||
} else {
|
||||
al.filepath = path.Join(cpath, "allow.list")
|
||||
al.filepath = filename
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,84 @@
|
||||
package cockraochdb_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
integration_tests "github.com/dosco/super-graph/core/internal/integration_tests"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCockroachDB(t *testing.T) {
|
||||
|
||||
dir, err := ioutil.TempDir("", "temp-cockraochdb-")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("cockroach", "start", "--insecure", "--listen-addr", ":0", "--http-addr", ":0", "--store=path="+dir)
|
||||
finder := &urlFinder{
|
||||
c: make(chan bool),
|
||||
}
|
||||
cmd.Stdout = finder
|
||||
cmd.Stderr = ioutil.Discard
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
t.Skip("is CockroachDB installed?: " + err.Error())
|
||||
}
|
||||
fmt.Println("started temporary cockroach db")
|
||||
|
||||
stopped := int32(0)
|
||||
stopDatabase := func() {
|
||||
fmt.Println("stopping temporary cockroach db")
|
||||
if atomic.CompareAndSwapInt32(&stopped, 0, 1) {
|
||||
cmd.Process.Kill()
|
||||
cmd.Process.Wait()
|
||||
os.RemoveAll(dir)
|
||||
}
|
||||
}
|
||||
defer stopDatabase()
|
||||
|
||||
// Wait till we figure out the URL we should connect to...
|
||||
<-finder.c
|
||||
db, err := sql.Open("pgx", finder.URL)
|
||||
if err != nil {
|
||||
stopDatabase()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration_tests.SetupSchema(t, db)
|
||||
|
||||
integration_tests.TestSuperGraph(t, db, func(t *testing.T) {
|
||||
if t.Name() == "TestCockroachDB/nested_insert" {
|
||||
t.Skip("nested inserts currently not working yet on cockroach db")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type urlFinder struct {
|
||||
c chan bool
|
||||
done bool
|
||||
URL string
|
||||
}
|
||||
|
||||
func (finder *urlFinder) Write(p []byte) (n int, err error) {
|
||||
s := string(p)
|
||||
urlRegex := regexp.MustCompile(`\nsql:\s+(postgresql:[^\s]+)\n`)
|
||||
if !finder.done {
|
||||
submatch := urlRegex.FindAllStringSubmatch(s, -1)
|
||||
if submatch != nil {
|
||||
finder.URL = submatch[0][1]
|
||||
finder.done = true
|
||||
close(finder.c)
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
143
core/internal/integration_tests/integration_tests.go
Normal file
@ -0,0 +1,143 @@
|
||||
package integration_tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/dosco/super-graph/core"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func SetupSchema(t *testing.T, db *sql.DB) {
|
||||
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE users (
|
||||
id integer PRIMARY KEY,
|
||||
full_name text
|
||||
)`)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE product (
|
||||
id integer PRIMARY KEY,
|
||||
name text,
|
||||
weight float
|
||||
)`)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE line_item (
|
||||
id integer PRIMARY KEY,
|
||||
product integer REFERENCES product(id),
|
||||
quantity integer,
|
||||
price float
|
||||
)`)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func DropSchema(t *testing.T, db *sql.DB) {
|
||||
|
||||
_, err := db.Exec(`DROP TABLE IF EXISTS line_item`)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Exec(`DROP TABLE IF EXISTS product`)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Exec(`DROP TABLE IF EXISTS users`)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSuperGraph(t *testing.T, db *sql.DB, before func(t *testing.T)) {
|
||||
config := core.Config{}
|
||||
config.UseAllowList = false
|
||||
config.AllowListFile = "./allow.list"
|
||||
config.RolesQuery = `SELECT * FROM users WHERE id = $user_id`
|
||||
|
||||
config.Roles = []core.Role{
|
||||
core.Role{
|
||||
Name: "anon",
|
||||
Tables: []core.RoleTable{
|
||||
core.RoleTable{Name: "users", Query: core.Query{Limit: 100}},
|
||||
core.RoleTable{Name: "product", Query: core.Query{Limit: 100}},
|
||||
core.RoleTable{Name: "line_item", Query: core.Query{Limit: 100}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sg, err := core.NewSuperGraph(&config, db)
|
||||
require.NoError(t, err)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("seed fixtures", func(t *testing.T) {
|
||||
before(t)
|
||||
res, err := sg.GraphQL(ctx,
|
||||
`mutation { products (insert: $products) { id } }`,
|
||||
json.RawMessage(`{"products":[
|
||||
{"id":1, "name":"Charmin Ultra Soft", "weight": 0.5},
|
||||
{"id":2, "name":"Hand Sanitizer", "weight": 0.2},
|
||||
{"id":3, "name":"Case of Corona", "weight": 1.2}
|
||||
]}`))
|
||||
require.NoError(t, err, res.SQL())
|
||||
require.Equal(t, `{"products": [{"id": 1}, {"id": 2}, {"id": 3}]}`, string(res.Data))
|
||||
|
||||
res, err = sg.GraphQL(ctx,
|
||||
`mutation { line_items (insert: $line_items) { id } }`,
|
||||
json.RawMessage(`{"line_items":[
|
||||
{"id":5001, "product":1, "price":6.95, "quantity":10},
|
||||
{"id":5002, "product":2, "price":10.99, "quantity":2}
|
||||
]}`))
|
||||
require.NoError(t, err, res.SQL())
|
||||
require.Equal(t, `{"line_items": [{"id": 5001}, {"id": 5002}]}`, string(res.Data))
|
||||
})
|
||||
|
||||
t.Run("get line items", func(t *testing.T) {
|
||||
before(t)
|
||||
res, err := sg.GraphQL(ctx,
|
||||
`query { line_items { id, price, quantity } }`,
|
||||
json.RawMessage(`{}`))
|
||||
require.NoError(t, err, res.SQL())
|
||||
require.Equal(t, `{"line_items": [{"id": 5001, "price": 6.95, "quantity": 10}, {"id": 5002, "price": 10.99, "quantity": 2}]}`, string(res.Data))
|
||||
})
|
||||
|
||||
t.Run("update line item", func(t *testing.T) {
|
||||
before(t)
|
||||
res, err := sg.GraphQL(ctx,
|
||||
`mutation { line_item(update:$update, id:$id) { id } }`,
|
||||
json.RawMessage(`{"id":5001, "update":{"quantity":20}}`))
|
||||
require.NoError(t, err, res.SQL())
|
||||
require.Equal(t, `{"line_item": {"id": 5001}}`, string(res.Data))
|
||||
|
||||
res, err = sg.GraphQL(ctx,
|
||||
`query { line_item(id:$id) { id, price, quantity } }`,
|
||||
json.RawMessage(`{"id":5001}`))
|
||||
require.NoError(t, err, res.SQL())
|
||||
require.Equal(t, `{"line_item": {"id": 5001, "price": 6.95, "quantity": 20}}`, string(res.Data))
|
||||
})
|
||||
|
||||
t.Run("delete line item", func(t *testing.T) {
|
||||
before(t)
|
||||
res, err := sg.GraphQL(ctx,
|
||||
`mutation { line_item(delete:true, id:$id) { id } }`,
|
||||
json.RawMessage(`{"id":5002}`))
|
||||
require.NoError(t, err, res.SQL())
|
||||
require.Equal(t, `{"line_item": {"id": 5002}}`, string(res.Data))
|
||||
|
||||
res, err = sg.GraphQL(ctx,
|
||||
`query { line_items { id, price, quantity } }`,
|
||||
json.RawMessage(`{}`))
|
||||
require.NoError(t, err, res.SQL())
|
||||
require.Equal(t, `{"line_items": [{"id": 5001, "price": 6.95, "quantity": 20}]}`, string(res.Data))
|
||||
})
|
||||
|
||||
t.Run("nested insert", func(t *testing.T) {
|
||||
before(t)
|
||||
res, err := sg.GraphQL(ctx,
|
||||
`mutation { line_items (insert: $line_item) { id, product { name } } }`,
|
||||
json.RawMessage(`{"line_item":
|
||||
{"id":5003, "product": { "connect": { "id": 1} }, "price":10.95, "quantity":15}
|
||||
}`))
|
||||
require.NoError(t, err, res.SQL())
|
||||
require.Equal(t, `{"line_items": [{"id": 5003, "product": {"name": "Charmin Ultra Soft"}}]}`, string(res.Data))
|
||||
})
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package cockraochdb_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
integration_tests "github.com/dosco/super-graph/core/internal/integration_tests"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCockroachDB(t *testing.T) {
|
||||
|
||||
url, found := os.LookupEnv("SG_POSTGRESQL_TEST_URL")
|
||||
if !found {
|
||||
t.Skip("set the SG_POSTGRESQL_TEST_URL env variable if you want to run integration tests against a PostgreSQL database")
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", url)
|
||||
require.NoError(t, err)
|
||||
|
||||
integration_tests.DropSchema(t, db)
|
||||
integration_tests.SetupSchema(t, db)
|
||||
integration_tests.TestSuperGraph(t, db, func(t *testing.T) {
|
||||
})
|
||||
}
|
@ -6,7 +6,7 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
)
|
||||
|
||||
func (c *compilerContext) renderBaseColumns(
|
||||
@ -35,33 +35,37 @@ func (c *compilerContext) renderBaseColumns(
|
||||
c.renderComma(i)
|
||||
realColsRendered = append(realColsRendered, n)
|
||||
colWithTable(c.w, ti.Name, cn)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if isSearch && !isRealCol {
|
||||
} else {
|
||||
switch {
|
||||
case cn == "search_rank":
|
||||
case isSearch && cn == "search_rank":
|
||||
if err := c.renderColumnSearchRank(sel, ti, col, i); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
i++
|
||||
|
||||
case strings.HasPrefix(cn, "search_headline_"):
|
||||
case isSearch && strings.HasPrefix(cn, "search_headline_"):
|
||||
if err := c.renderColumnSearchHeadline(sel, ti, col, i); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
i++
|
||||
|
||||
}
|
||||
} else {
|
||||
if err := c.renderColumnFunction(sel, ti, col, i); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
isAgg = true
|
||||
i++
|
||||
case cn == "__typename":
|
||||
if err := c.renderColumnTypename(sel, ti, col, i); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
case strings.HasSuffix(cn, "_cursor"):
|
||||
continue
|
||||
|
||||
default:
|
||||
if err := c.renderColumnFunction(sel, ti, col, i); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
isAgg = true
|
||||
}
|
||||
}
|
||||
i++
|
||||
|
||||
}
|
||||
|
||||
if isCursorPaged {
|
||||
@ -148,6 +152,20 @@ func (c *compilerContext) renderColumnSearchHeadline(sel *qcode.Select, ti *DBTa
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderColumnTypename(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
|
||||
if isColumnBlocked(sel, col.Name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.renderComma(columnsRendered)
|
||||
io.WriteString(c.w, `(`)
|
||||
squoted(c.w, ti.Name)
|
||||
io.WriteString(c.w, ` :: text)`)
|
||||
alias(c.w, col.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderColumnFunction(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error {
|
||||
pl := funcPrefixLen(col.Name)
|
||||
// if pl == 0 {
|
||||
@ -168,7 +186,7 @@ func (c *compilerContext) renderColumnFunction(sel *qcode.Select, ti *DBTableInf
|
||||
return nil
|
||||
}
|
||||
|
||||
fn := cn[0 : pl-1]
|
||||
fn := col.Name[:pl-1]
|
||||
|
||||
c.renderComma(columnsRendered)
|
||||
|
@ -4,7 +4,7 @@ package psql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
)
|
||||
|
||||
var (
|
@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/util"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
"github.com/dosco/super-graph/core/internal/util"
|
||||
)
|
||||
|
||||
func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer,
|
||||
@ -21,9 +21,17 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer,
|
||||
return 0, fmt.Errorf("variable '%s' is empty", qc.ActionVar)
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
|
||||
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT `)
|
||||
if insert[0] == '[' {
|
||||
io.WriteString(c.w, `json_array_elements(`)
|
||||
}
|
||||
io.WriteString(c.w, `'{{`)
|
||||
io.WriteString(c.w, qc.ActionVar)
|
||||
io.WriteString(c.w, `}}' :: json AS j)`)
|
||||
io.WriteString(c.w, `}}' :: json`)
|
||||
if insert[0] == '[' {
|
||||
io.WriteString(c.w, `)`)
|
||||
}
|
||||
io.WriteString(c.w, ` AS j)`)
|
||||
|
||||
st := util.NewStack()
|
||||
st.Push(kvitem{_type: itemInsert, key: ti.Name, val: insert, ti: ti})
|
||||
@ -90,26 +98,9 @@ func (c *compilerContext) renderInsertStmt(qc *qcode.QCode, w io.Writer, item re
|
||||
renderInsertUpdateColumns(w, qc, jt, ti, sk, true)
|
||||
renderNestedInsertRelColumns(w, item.kvitem, true)
|
||||
|
||||
io.WriteString(w, ` FROM "_sg_input" i, `)
|
||||
io.WriteString(w, ` FROM "_sg_input" i`)
|
||||
renderNestedInsertRelTables(w, item.kvitem)
|
||||
|
||||
if item.array {
|
||||
io.WriteString(w, `json_populate_recordset`)
|
||||
} else {
|
||||
io.WriteString(w, `json_populate_record`)
|
||||
}
|
||||
|
||||
io.WriteString(w, `(NULL::`)
|
||||
io.WriteString(w, ti.Name)
|
||||
|
||||
if len(item.path) == 0 {
|
||||
io.WriteString(w, `, i.j) t RETURNING *)`)
|
||||
} else {
|
||||
io.WriteString(w, `, i.j->`)
|
||||
joinPath(w, item.path)
|
||||
io.WriteString(w, `) t RETURNING *)`)
|
||||
}
|
||||
|
||||
io.WriteString(w, ` RETURNING *)`)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -172,21 +163,21 @@ func renderNestedInsertRelColumns(w io.Writer, item kvitem, values bool) error {
|
||||
func renderNestedInsertRelTables(w io.Writer, item kvitem) error {
|
||||
if len(item.items) == 0 {
|
||||
if item.relPC != nil && item.relPC.Type == RelOneToMany {
|
||||
quoted(w, item.relPC.Left.Table)
|
||||
io.WriteString(w, `, `)
|
||||
quoted(w, item.relPC.Left.Table)
|
||||
}
|
||||
} else {
|
||||
// Render tables needed to set values if child-to-parent
|
||||
// relationship is one-to-many
|
||||
for _, v := range item.items {
|
||||
if v.relCP.Type == RelOneToMany {
|
||||
io.WriteString(w, `, `)
|
||||
if v._ctype > 0 {
|
||||
io.WriteString(w, `"_x_`)
|
||||
io.WriteString(w, v.relCP.Left.Table)
|
||||
io.WriteString(w, `", `)
|
||||
io.WriteString(w, `"`)
|
||||
} else {
|
||||
quoted(w, v.relCP.Left.Table)
|
||||
io.WriteString(w, `, `)
|
||||
}
|
||||
}
|
||||
}
|
@ -7,9 +7,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
"github.com/dosco/super-graph/core/internal/util"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/util"
|
||||
)
|
||||
|
||||
type itemType int
|
||||
@ -396,7 +396,12 @@ func renderInsertUpdateColumns(w io.Writer,
|
||||
}
|
||||
|
||||
if values {
|
||||
colWithTable(w, "t", cn.Name)
|
||||
io.WriteString(w, `CAST( i.j ->>`)
|
||||
io.WriteString(w, `'`)
|
||||
io.WriteString(w, cn.Name)
|
||||
io.WriteString(w, `' AS `)
|
||||
io.WriteString(w, cn.Type)
|
||||
io.WriteString(w, `)`)
|
||||
} else {
|
||||
quoted(w, cn.Name)
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
)
|
||||
|
||||
const (
|
@ -9,14 +9,18 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/util"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
"github.com/dosco/super-graph/core/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
closeBlock = 500
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAllTablesSkipped = errors.New("all tables skipped. cannot render query")
|
||||
)
|
||||
|
||||
type Variables map[string]json.RawMessage
|
||||
|
||||
type Config struct {
|
||||
@ -86,10 +90,10 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (
|
||||
st := NewIntStack()
|
||||
i := 0
|
||||
|
||||
io.WriteString(c.w, `SELECT json_build_object(`)
|
||||
io.WriteString(c.w, `SELECT jsonb_build_object(`)
|
||||
for _, id := range qc.Roots {
|
||||
root := &qc.Selects[id]
|
||||
if root.SkipRender {
|
||||
if root.SkipRender || len(root.Cols) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -107,7 +111,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (
|
||||
io.WriteString(c.w, `) as "__root" FROM `)
|
||||
|
||||
if i == 0 {
|
||||
return 0, errors.New("all tables skipped. cannot render query")
|
||||
return 0, ErrAllTablesSkipped
|
||||
}
|
||||
|
||||
var ignored uint32
|
||||
@ -122,6 +126,10 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (
|
||||
if id < closeBlock {
|
||||
sel := &c.s[id]
|
||||
|
||||
if len(sel.Cols) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ti, err := c.schema.GetTable(sel.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@ -164,15 +172,18 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (
|
||||
return 0, err
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "__sr", sel.ID)
|
||||
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "__sj", sel.ID)
|
||||
|
||||
if !ti.Singular {
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "__sel", sel.ID)
|
||||
aliasWithID(c.w, "__sj", sel.ID)
|
||||
}
|
||||
|
||||
if sel.ParentID == -1 {
|
||||
io.WriteString(c.w, `)`)
|
||||
aliasWithID(c.w, "__sel", sel.ID)
|
||||
|
||||
if st.Len() != 0 {
|
||||
io.WriteString(c.w, `, `)
|
||||
}
|
||||
@ -194,7 +205,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) (
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderPluralSelect(sel *qcode.Select, ti *DBTableInfo) error {
|
||||
io.WriteString(c.w, `SELECT coalesce(json_agg("__sel_`)
|
||||
io.WriteString(c.w, `SELECT coalesce(jsonb_agg("__sj_`)
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `"."json"), '[]') as "json"`)
|
||||
|
||||
@ -234,7 +245,7 @@ func (c *compilerContext) renderRootSelect(sel *qcode.Select) error {
|
||||
io.WriteString(c.w, sel.FieldName)
|
||||
io.WriteString(c.w, `', `)
|
||||
|
||||
io.WriteString(c.w, `"__sel_`)
|
||||
io.WriteString(c.w, `"__sj_`)
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `"."json"`)
|
||||
|
||||
@ -243,7 +254,7 @@ func (c *compilerContext) renderRootSelect(sel *qcode.Select) error {
|
||||
io.WriteString(c.w, sel.FieldName)
|
||||
io.WriteString(c.w, `_cursor', `)
|
||||
|
||||
io.WriteString(c.w, `"__sel_`)
|
||||
io.WriteString(c.w, `"__sj_`)
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `"."cursor"`)
|
||||
}
|
||||
@ -420,11 +431,38 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars
|
||||
}
|
||||
|
||||
// SELECT
|
||||
io.WriteString(c.w, `SELECT json_build_object(`)
|
||||
// io.WriteString(c.w, `SELECT jsonb_build_object(`)
|
||||
// if err := c.renderColumns(sel, ti, skipped); err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
|
||||
io.WriteString(c.w, `SELECT to_jsonb("__sr_`)
|
||||
int2string(c.w, sel.ID)
|
||||
io.WriteString(c.w, `".*) `)
|
||||
|
||||
if sel.Paging.Type != qcode.PtOffset {
|
||||
for i := range sel.OrderBy {
|
||||
io.WriteString(c.w, `- '__cur_`)
|
||||
int2string(c.w, int32(i))
|
||||
io.WriteString(c.w, `' `)
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `AS "json"`)
|
||||
|
||||
if sel.Paging.Type != qcode.PtOffset {
|
||||
for i := range sel.OrderBy {
|
||||
io.WriteString(c.w, `, "__cur_`)
|
||||
int2string(c.w, int32(i))
|
||||
io.WriteString(c.w, `"`)
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `FROM (SELECT `)
|
||||
|
||||
if err := c.renderColumns(sel, ti, skipped); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
io.WriteString(c.w, `) AS "json"`)
|
||||
|
||||
if sel.Paging.Type != qcode.PtOffset {
|
||||
for i, ob := range sel.OrderBy {
|
||||
@ -459,8 +497,8 @@ func (c *compilerContext) renderLateralJoin(sel *qcode.Select) error {
|
||||
}
|
||||
|
||||
func (c *compilerContext) renderLateralJoinClose(sel *qcode.Select) error {
|
||||
io.WriteString(c.w, `) `)
|
||||
aliasWithID(c.w, "__sel", sel.ID)
|
||||
// io.WriteString(c.w, `) `)
|
||||
// aliasWithID(c.w, "__sj", sel.ID)
|
||||
io.WriteString(c.w, ` ON ('true')`)
|
||||
return nil
|
||||
}
|
||||
@ -502,22 +540,25 @@ func (c *compilerContext) renderJoinByName(table, parent string, id int32) error
|
||||
|
||||
func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error {
|
||||
i := 0
|
||||
var cn string
|
||||
|
||||
for _, col := range sel.Cols {
|
||||
n := funcPrefixLen(col.Name)
|
||||
if n != 0 {
|
||||
if n := funcPrefixLen(col.Name); n != 0 {
|
||||
if !sel.Functions {
|
||||
continue
|
||||
}
|
||||
if len(sel.Allowed) != 0 {
|
||||
if _, ok := sel.Allowed[col.Name[n:]]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
cn = col.Name[n:]
|
||||
} else {
|
||||
if len(sel.Allowed) != 0 {
|
||||
if _, ok := sel.Allowed[col.Name]; !ok {
|
||||
continue
|
||||
}
|
||||
cn = col.Name
|
||||
|
||||
if strings.HasSuffix(cn, "_cursor") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(sel.Allowed) != 0 {
|
||||
if _, ok := sel.Allowed[cn]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@ -525,9 +566,8 @@ func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo, skip
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
|
||||
squoted(c.w, col.FieldName)
|
||||
io.WriteString(c.w, ", ")
|
||||
colWithTableID(c.w, ti.Name, sel.ID, col.Name)
|
||||
alias(c.w, col.FieldName)
|
||||
|
||||
i++
|
||||
}
|
||||
@ -551,9 +591,8 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableI
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
|
||||
squoted(c.w, rel.Right.Col)
|
||||
io.WriteString(c.w, ", ")
|
||||
colWithTableID(c.w, ti.Name, sel.ID, rel.Left.Col)
|
||||
alias(c.w, rel.Right.Col)
|
||||
i++
|
||||
}
|
||||
|
||||
@ -569,26 +608,28 @@ func (c *compilerContext) renderJoinColumns(sel *qcode.Select, ti *DBTableInfo,
|
||||
continue
|
||||
}
|
||||
childSel := &c.s[id]
|
||||
if childSel.SkipRender {
|
||||
continue
|
||||
}
|
||||
|
||||
if i != 0 {
|
||||
io.WriteString(c.w, ", ")
|
||||
}
|
||||
|
||||
squoted(c.w, childSel.FieldName)
|
||||
if childSel.SkipRender {
|
||||
io.WriteString(c.w, `NULL`)
|
||||
alias(c.w, childSel.FieldName)
|
||||
continue
|
||||
}
|
||||
|
||||
io.WriteString(c.w, `, "__sel_`)
|
||||
io.WriteString(c.w, `"__sj_`)
|
||||
int2string(c.w, childSel.ID)
|
||||
io.WriteString(c.w, `"."json"`)
|
||||
alias(c.w, childSel.FieldName)
|
||||
|
||||
if childSel.Paging.Type != qcode.PtOffset {
|
||||
io.WriteString(c.w, `, '`)
|
||||
io.WriteString(c.w, childSel.FieldName)
|
||||
io.WriteString(c.w, `_cursor', "__sel_`)
|
||||
io.WriteString(c.w, `, "__sj_`)
|
||||
int2string(c.w, childSel.ID)
|
||||
io.WriteString(c.w, `"."cursor"`)
|
||||
io.WriteString(c.w, `"."cursor" AS "`)
|
||||
io.WriteString(c.w, childSel.FieldName)
|
||||
io.WriteString(c.w, `_cursor"`)
|
||||
}
|
||||
|
||||
i++
|
||||
@ -693,7 +734,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r
|
||||
|
||||
func (c *compilerContext) renderFrom(sel *qcode.Select, ti *DBTableInfo, rel *DBRel) error {
|
||||
if rel != nil && rel.Type == RelEmbedded {
|
||||
// json_to_recordset('[{"a":1,"b":[1,2,3],"c":"bar"}, {"a":2,"b":[1,2,3],"c":"bar"}]') as x(a int, b text, d text);
|
||||
// jsonb_to_recordset('[{"a":1,"b":[1,2,3],"c":"bar"}, {"a":2,"b":[1,2,3],"c":"bar"}]') as x(a int, b text, d text);
|
||||
|
||||
io.WriteString(c.w, `"`)
|
||||
io.WriteString(c.w, rel.Left.Table)
|
@ -327,7 +327,7 @@ func jsonColumnAsTable(t *testing.T) {
|
||||
compileGQLToPSQL(t, gql, nil, "admin")
|
||||
}
|
||||
|
||||
func skipUserIDForAnonRole(t *testing.T) {
|
||||
func nullForAuthRequiredInAnon(t *testing.T) {
|
||||
gql := `query {
|
||||
products {
|
||||
id
|
||||
@ -387,7 +387,7 @@ func TestCompileQuery(t *testing.T) {
|
||||
t.Run("multiRoot", multiRoot)
|
||||
t.Run("jsonColumnAsTable", jsonColumnAsTable)
|
||||
t.Run("withCursor", withCursor)
|
||||
t.Run("skipUserIDForAnonRole", skipUserIDForAnonRole)
|
||||
t.Run("nullForAuthRequiredInAnon", nullForAuthRequiredInAnon)
|
||||
t.Run("blockedQuery", blockedQuery)
|
||||
t.Run("blockedFunctions", blockedFunctions)
|
||||
}
|
@ -66,7 +66,14 @@ func NewDBSchema(info *DBInfo, aliases map[string][]string) (*DBSchema, error) {
|
||||
}
|
||||
|
||||
for i, t := range info.Tables {
|
||||
err := schema.updateRelationships(t, info.Columns[i])
|
||||
err := schema.firstDegreeRels(t, info.Columns[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range info.Tables {
|
||||
err := schema.secondDegreeRels(t, info.Columns[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -131,8 +138,7 @@ func (s *DBSchema) addTable(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
|
||||
jcols := make([]DBColumn, 0, len(cols))
|
||||
func (s *DBSchema) firstDegreeRels(t DBTable, cols []DBColumn) error {
|
||||
ct := t.Key
|
||||
cti, ok := s.t[ct]
|
||||
if !ok {
|
||||
@ -230,6 +236,51 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
|
||||
if err := s.SetRel(ft, ct, rel2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DBSchema) secondDegreeRels(t DBTable, cols []DBColumn) error {
|
||||
jcols := make([]DBColumn, 0, len(cols))
|
||||
ct := t.Key
|
||||
cti, ok := s.t[ct]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid foreign key table '%s'", ct)
|
||||
}
|
||||
|
||||
for i := range cols {
|
||||
c := cols[i]
|
||||
|
||||
if len(c.FKeyTable) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Foreign key column name
|
||||
ft := strings.ToLower(c.FKeyTable)
|
||||
|
||||
ti, ok := s.t[ft]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid foreign key table '%s'", ft)
|
||||
}
|
||||
|
||||
// This is an embedded relationship like when a json/jsonb column
|
||||
// is exposed as a table
|
||||
if c.Name == c.FKeyTable && len(c.FKeyColID) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(c.FKeyColID) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Foreign key column id
|
||||
fcid := c.FKeyColID[0]
|
||||
|
||||
if _, ok := ti.ColIDMap[fcid]; !ok {
|
||||
return fmt.Errorf("invalid foreign key column id '%d' for table '%s'",
|
||||
fcid, ti.Name)
|
||||
}
|
||||
|
||||
jcols = append(jcols, c)
|
||||
}
|
||||
@ -322,6 +373,9 @@ func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {
|
||||
}
|
||||
|
||||
func (s *DBSchema) SetRel(child, parent string, rel *DBRel) error {
|
||||
sp := strings.ToLower(flect.Singularize(parent))
|
||||
pp := strings.ToLower(flect.Pluralize(parent))
|
||||
|
||||
sc := strings.ToLower(flect.Singularize(child))
|
||||
pc := strings.ToLower(flect.Pluralize(child))
|
||||
|
||||
@ -333,9 +387,6 @@ func (s *DBSchema) SetRel(child, parent string, rel *DBRel) error {
|
||||
s.rm[pc] = make(map[string]*DBRel)
|
||||
}
|
||||
|
||||
sp := strings.ToLower(flect.Singularize(parent))
|
||||
pp := strings.ToLower(flect.Pluralize(parent))
|
||||
|
||||
if _, ok := s.rm[sc][sp]; !ok {
|
||||
s.rm[sc][sp] = rel
|
||||
}
|
@ -19,6 +19,10 @@ func (rt RelType) String() string {
|
||||
}
|
||||
|
||||
func (re *DBRel) String() string {
|
||||
if re.Type == RelOneToManyThrough {
|
||||
return fmt.Sprintf("'%s.%s' --(Through: %s)--> '%s.%s'",
|
||||
re.Left.Table, re.Left.Col, re.Through, re.Right.Table, re.Right.Col)
|
||||
}
|
||||
return fmt.Sprintf("'%s.%s' --(%s)--> '%s.%s'",
|
||||
re.Left.Table, re.Left.Col, re.Type, re.Right.Table, re.Right.Col)
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
package psql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
)
|
||||
|
||||
type DBInfo struct {
|
||||
@ -17,18 +16,11 @@ type DBInfo struct {
|
||||
colmap map[string]map[string]*DBColumn
|
||||
}
|
||||
|
||||
func GetDBInfo(db *pgxpool.Pool) (*DBInfo, error) {
|
||||
func GetDBInfo(db *sql.DB) (*DBInfo, error) {
|
||||
di := &DBInfo{}
|
||||
|
||||
dbc, err := db.Acquire(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error acquiring connection from pool: %w", err)
|
||||
}
|
||||
defer dbc.Release()
|
||||
|
||||
var version string
|
||||
|
||||
err = dbc.QueryRow(context.Background(), `SHOW server_version_num`).Scan(&version)
|
||||
err := db.QueryRow(`SHOW server_version_num`).Scan(&version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching version: %w", err)
|
||||
}
|
||||
@ -38,7 +30,7 @@ func GetDBInfo(db *pgxpool.Pool) (*DBInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
di.Tables, err = GetTables(dbc)
|
||||
di.Tables, err = GetTables(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -46,7 +38,7 @@ func GetDBInfo(db *pgxpool.Pool) (*DBInfo, error) {
|
||||
di.colmap = make(map[string]map[string]*DBColumn, len(di.Tables))
|
||||
|
||||
for i, t := range di.Tables {
|
||||
cols, err := GetColumns(dbc, "public", t.Name)
|
||||
cols, err := GetColumns(db, "public", t.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -88,7 +80,7 @@ type DBTable struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
func GetTables(dbc *pgxpool.Conn) ([]DBTable, error) {
|
||||
func GetTables(db *sql.DB) ([]DBTable, error) {
|
||||
sqlStmt := `
|
||||
SELECT
|
||||
c.relname as "name",
|
||||
@ -107,7 +99,7 @@ AND pg_catalog.pg_table_is_visible(c.oid);`
|
||||
|
||||
var tables []DBTable
|
||||
|
||||
rows, err := dbc.Query(context.Background(), sqlStmt)
|
||||
rows, err := db.Query(sqlStmt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching tables: %s", err)
|
||||
}
|
||||
@ -142,7 +134,7 @@ type DBColumn struct {
|
||||
fKeyColID pgtype.Int2Array
|
||||
}
|
||||
|
||||
func GetColumns(dbc *pgxpool.Conn, schema, table string) ([]DBColumn, error) {
|
||||
func GetColumns(db *sql.DB, schema, table string) ([]DBColumn, error) {
|
||||
sqlStmt := `
|
||||
SELECT
|
||||
f.attnum AS id,
|
||||
@ -183,7 +175,7 @@ WHERE c.relkind IN ('r', 'v', 'm', 'f')
|
||||
AND f.attisdropped = false
|
||||
ORDER BY id;`
|
||||
|
||||
rows, err := dbc.Query(context.Background(), sqlStmt, schema, table)
|
||||
rows, err := db.Query(sqlStmt, schema, table)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching columns: %s", err)
|
||||
}
|
@ -92,7 +92,14 @@ func getTestSchema() *DBSchema {
|
||||
}
|
||||
|
||||
for i, t := range tables {
|
||||
err := schema.updateRelationships(t, columns[i])
|
||||
err := schema.firstDegreeRels(t, columns[i])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range tables {
|
||||
err := schema.secondDegreeRels(t, columns[i])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
288
core/internal/psql/tests.sql
Normal file
@ -0,0 +1,288 @@
|
||||
? github.com/dosco/super-graph [no test files]
|
||||
testing: warning: no tests to run
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/core 0.025s [no tests to run]
|
||||
=== RUN TestGQLName1
|
||||
--- PASS: TestGQLName1 (0.00s)
|
||||
=== RUN TestGQLName2
|
||||
--- PASS: TestGQLName2 (0.00s)
|
||||
=== RUN TestGQLName3
|
||||
--- PASS: TestGQLName3 (0.00s)
|
||||
=== RUN TestGQLName4
|
||||
--- PASS: TestGQLName4 (0.00s)
|
||||
=== RUN TestGQLName5
|
||||
--- PASS: TestGQLName5 (0.00s)
|
||||
=== RUN TestFuzzCrashers
|
||||
--- PASS: TestFuzzCrashers (0.00s)
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/core/internal/allow 0.010s
|
||||
? github.com/dosco/super-graph/core/internal/crypto [no test files]
|
||||
? github.com/dosco/super-graph/core/internal/integration_tests [no test files]
|
||||
=== RUN TestCockroachDB
|
||||
started temporary cockroach db
|
||||
=== RUN TestCockroachDB/seed_fixtures
|
||||
=== RUN TestCockroachDB/get_line_items
|
||||
=== RUN TestCockroachDB/update_line_item
|
||||
=== RUN TestCockroachDB/delete_line_item
|
||||
=== RUN TestCockroachDB/nested_insert
|
||||
stopping temporary cockroach db
|
||||
--- PASS: TestCockroachDB (0.85s)
|
||||
--- PASS: TestCockroachDB/seed_fixtures (0.01s)
|
||||
--- PASS: TestCockroachDB/get_line_items (0.00s)
|
||||
--- PASS: TestCockroachDB/update_line_item (0.00s)
|
||||
--- PASS: TestCockroachDB/delete_line_item (0.00s)
|
||||
--- SKIP: TestCockroachDB/nested_insert (0.00s)
|
||||
cockroachdb_test.go:61: nested inserts currently not working yet on cockroach db
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/core/internal/integration_tests/cockroachdb 0.872s
|
||||
=== RUN TestCockroachDB
|
||||
--- SKIP: TestCockroachDB (0.00s)
|
||||
postgresql_test.go:17: set the SG_POSTGRESQL_TEST_URL env variable if you want to run integration tests against a PostgreSQL database
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/core/internal/integration_tests/postgresql 0.015s
|
||||
=== RUN TestCompileInsert
|
||||
=== RUN TestCompileInsert/simpleInsert
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i RETURNING *) SELECT jsonb_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/singleInsert
|
||||
WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'user_id' AS bigint) FROM "_sg_input" i RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/bulkInsert
|
||||
WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/simpleInsertWithPresets
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM "_sg_input" i RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/nestedInsertManyToMany
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT CAST( i.j ->>'sale_type' AS character varying), CAST( i.j ->>'quantity' AS integer), CAST( i.j ->>'due_date' AS timestamp without time zone), "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products" RETURNING *) SELECT jsonb_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i RETURNING *), "products" AS (INSERT INTO "products" ("name", "price") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "product_id", "customer_id") SELECT CAST( i.j ->>'sale_type' AS character varying), CAST( i.j ->>'quantity' AS integer), CAST( i.j ->>'due_date' AS timestamp without time zone), "products"."id", "customers"."id" FROM "_sg_input" i, "products", "customers" RETURNING *) SELECT jsonb_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToMany
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone), "users"."id" FROM "_sg_input" i, "users" RETURNING *) SELECT jsonb_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToOne
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i RETURNING *), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone), "users"."id" FROM "_sg_input" i, "users" RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToManyWithConnect
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*) SELECT jsonb_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToOneWithConnect
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone), "_x_users"."id" FROM "_sg_input" i, "_x_users" RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user", "__sj_2"."json" AS "tags" FROM (SELECT "products"."id", "products"."name", "products"."user_id", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "tags_2"."id" AS "id", "tags_2"."name" AS "name" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileInsert/nestedInsertOneToOneWithConnectArray
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id" = ANY((select a::bigint AS list from json_array_elements_text((i.j->'user'->'connect'->>'id')::json) AS a)) LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone), "_x_users"."id" FROM "_sg_input" i, "_x_users" RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
--- PASS: TestCompileInsert (0.04s)
|
||||
--- PASS: TestCompileInsert/simpleInsert (0.00s)
|
||||
--- PASS: TestCompileInsert/singleInsert (0.00s)
|
||||
--- PASS: TestCompileInsert/bulkInsert (0.00s)
|
||||
--- PASS: TestCompileInsert/simpleInsertWithPresets (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertManyToMany (0.01s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToMany (0.01s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToOne (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToManyWithConnect (0.00s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToOneWithConnect (0.01s)
|
||||
--- PASS: TestCompileInsert/nestedInsertOneToOneWithConnectArray (0.01s)
|
||||
=== RUN TestCompileMutate
|
||||
=== RUN TestCompileMutate/singleUpsert
|
||||
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileMutate/singleUpsertWhere
|
||||
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) WHERE (("products"."price") > '3' :: numeric(7,2)) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileMutate/bulkUpsert
|
||||
WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileMutate/delete
|
||||
WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '1' :: bigint)) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
--- PASS: TestCompileMutate (0.01s)
|
||||
--- PASS: TestCompileMutate/singleUpsert (0.00s)
|
||||
--- PASS: TestCompileMutate/singleUpsertWhere (0.00s)
|
||||
--- PASS: TestCompileMutate/bulkUpsert (0.00s)
|
||||
--- PASS: TestCompileMutate/delete (0.00s)
|
||||
=== RUN TestCompileQuery
|
||||
=== RUN TestCompileQuery/withComplexArgs
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price" FROM (SELECT DISTINCT ON ("products"."price") "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."id") < '28' :: bigint) AND (("products"."id") >= '20' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) ORDER BY "products"."price" DESC LIMIT ('30') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/withWhereAndList
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/withWhereIsNull
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE (((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/withWhereMultiOr
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") < '20' :: numeric(7,2)) OR (("products"."price") > '10' :: numeric(7,2)) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/fetchByID
|
||||
SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '{{id}}' :: bigint))) LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/searchQuery
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."search_rank" AS "search_rank", "products_0"."search_headline_description" AS "search_headline_description" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('{{query}}')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('{{query}}')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('{{query}}'))) LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/oneToMany
|
||||
SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."email" AS "email", "__sj_1"."json" AS "products" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/oneToManyReverse
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "__sj_1"."json" AS "users" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."email" AS "email" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/oneToManyArray
|
||||
SELECT jsonb_build_object('tags', "__sj_0"."json", 'product', "__sj_2"."json") as "__root" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "products_2"."name" AS "name", "products_2"."price" AS "price", "__sj_3"."json" AS "tags" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_3"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "tags_3"."id" AS "id", "tags_3"."name" AS "name" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3") AS "__sr_3") AS "__sj_3") AS "__sj_3" ON ('true')) AS "__sr_2") AS "__sj_2", (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "tags_0"."name" AS "name", "__sj_1"."json" AS "product" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."name" AS "name" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/manyToMany
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."name" AS "name", "__sj_1"."json" AS "customers" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/manyToManyReverse
|
||||
SELECT jsonb_build_object('customers', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "__sj_1"."json" AS "products" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_1"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."name" AS "name" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/aggFunction
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/aggFunctionBlockedByCol
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."name" AS "name" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/aggFunctionDisabled
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."name" AS "name" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/aggFunctionWithFilter
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."max_price" AS "max_price" FROM (SELECT "products"."id", max("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") > '10' :: bigint))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/syntheticTables
|
||||
SELECT jsonb_build_object('me', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/queryWithVariables
|
||||
SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") = '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/withWhereOnRelations
|
||||
SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > '3' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/multiRoot
|
||||
SELECT jsonb_build_object('customer', "__sj_0"."json", 'user', "__sj_1"."json", 'product', "__sj_2"."json") as "__root" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "products_2"."id" AS "id", "products_2"."name" AS "name", "__sj_3"."json" AS "customers", "__sj_4"."json" AS "customer" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_4".*) AS "json"FROM (SELECT "customers_4"."email" AS "email" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4") AS "__sr_4") AS "__sj_4" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_3"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_3".*) AS "json"FROM (SELECT "customers_3"."email" AS "email" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3") AS "__sr_3") AS "__sj_3") AS "__sj_3" ON ('true')) AS "__sr_2") AS "__sj_2", (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1", (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "customers_0"."id" AS "id" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/jsonColumnAsTable
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "tag_count" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "tag_count_1"."count" AS "count", "__sj_2"."json" AS "tags" FROM (SELECT "tag_count"."count", "tag_count"."tag_id" FROM "products", json_to_recordset("products"."tag_count") AS "tag_count"(tag_id bigint, count int) WHERE ((("products"."id") = ("products_0"."id"))) LIMIT ('1') :: integer) AS "tag_count_1" LEFT OUTER JOIN LATERAL (SELECT coalesce(jsonb_agg("__sj_2"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "tags_2"."name" AS "name" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2") AS "__sr_2") AS "__sj_2") AS "__sj_2" ON ('true')) AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/withCursor
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json", 'products_cursor', "__sj_0"."cursor") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json", CONCAT_WS(',', max("__cur_0"), max("__cur_1")) as "cursor" FROM (SELECT to_jsonb("__sr_0".*) - '__cur_0' - '__cur_1' AS "json", "__cur_0", "__cur_1"FROM (SELECT "products_0"."name" AS "name", LAST_VALUE("products_0"."price") OVER() AS "__cur_0", LAST_VALUE("products_0"."id") OVER() AS "__cur_1" FROM (WITH "__cur" AS (SELECT a[1] as "price", a[2] as "id" FROM string_to_array('{{cursor}}', ',') as a) SELECT "products"."name", "products"."id", "products"."price" FROM "products", "__cur" WHERE (((("products"."price") < "__cur"."price" :: numeric(7,2)) OR ((("products"."price") = "__cur"."price" :: numeric(7,2)) AND (("products"."id") > "__cur"."id" :: bigint)))) ORDER BY "products"."price" DESC, "products"."id" ASC LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/nullForAuthRequiredInAnon
|
||||
SELECT jsonb_build_object('products', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", NULL AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/blockedQuery
|
||||
SELECT jsonb_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileQuery/blockedFunctions
|
||||
SELECT jsonb_build_object('users', "__sj_0"."json") as "__root" FROM (SELECT coalesce(jsonb_agg("__sj_0"."json"), '[]') as "json" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."email" AS "email" FROM (SELECT , "users"."email" FROM "users" WHERE (false) GROUP BY "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "__sr_0") AS "__sj_0") AS "__sj_0"
|
||||
--- PASS: TestCompileQuery (0.05s)
|
||||
--- PASS: TestCompileQuery/withComplexArgs (0.00s)
|
||||
--- PASS: TestCompileQuery/withWhereAndList (0.00s)
|
||||
--- PASS: TestCompileQuery/withWhereIsNull (0.00s)
|
||||
--- PASS: TestCompileQuery/withWhereMultiOr (0.00s)
|
||||
--- PASS: TestCompileQuery/fetchByID (0.00s)
|
||||
--- PASS: TestCompileQuery/searchQuery (0.00s)
|
||||
--- PASS: TestCompileQuery/oneToMany (0.00s)
|
||||
--- PASS: TestCompileQuery/oneToManyReverse (0.00s)
|
||||
--- PASS: TestCompileQuery/oneToManyArray (0.00s)
|
||||
--- PASS: TestCompileQuery/manyToMany (0.00s)
|
||||
--- PASS: TestCompileQuery/manyToManyReverse (0.00s)
|
||||
--- PASS: TestCompileQuery/aggFunction (0.00s)
|
||||
--- PASS: TestCompileQuery/aggFunctionBlockedByCol (0.00s)
|
||||
--- PASS: TestCompileQuery/aggFunctionDisabled (0.00s)
|
||||
--- PASS: TestCompileQuery/aggFunctionWithFilter (0.00s)
|
||||
--- PASS: TestCompileQuery/syntheticTables (0.00s)
|
||||
--- PASS: TestCompileQuery/queryWithVariables (0.00s)
|
||||
--- PASS: TestCompileQuery/withWhereOnRelations (0.00s)
|
||||
--- PASS: TestCompileQuery/multiRoot (0.01s)
|
||||
--- PASS: TestCompileQuery/jsonColumnAsTable (0.00s)
|
||||
--- PASS: TestCompileQuery/withCursor (0.00s)
|
||||
--- PASS: TestCompileQuery/nullForAuthRequiredInAnon (0.00s)
|
||||
--- PASS: TestCompileQuery/blockedQuery (0.00s)
|
||||
--- PASS: TestCompileQuery/blockedFunctions (0.00s)
|
||||
=== RUN TestCompileUpdate
|
||||
=== RUN TestCompileUpdate/singleUpdate
|
||||
WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'description' AS text) FROM "_sg_input" i) WHERE ((("products"."id") = '1' :: bigint) AND (("products"."id") = '{{id}}' :: bigint)) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileUpdate/simpleUpdateWithPresets
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), 'now' :: timestamp without time zone FROM "_sg_input" i) WHERE (("products"."user_id") = '{{user_id}}' :: bigint) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateManyToMany
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT CAST( i.j ->>'sale_type' AS character varying), CAST( i.j ->>'quantity' AS integer), CAST( i.j ->>'due_date' AS timestamp without time zone) FROM "_sg_input" i) WHERE (("purchases"."id") = '{{id}}' :: bigint) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT jsonb_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT CAST( i.j ->>'sale_type' AS character varying), CAST( i.j ->>'quantity' AS integer), CAST( i.j ->>'due_date' AS timestamp without time zone) FROM "_sg_input" i) WHERE (("purchases"."id") = '{{id}}' :: bigint) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)) FROM "_sg_input" i) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT jsonb_build_object('purchase', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "__sj_1"."json" AS "product", "__sj_2"."json" AS "customer" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_2".*) AS "json"FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "__sr_2") AS "__sj_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToMany
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i) WHERE (("users"."id") = '8' :: bigint) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id"= ((i.j->'product'->'where'->>'id'))::bigint) RETURNING "products".*) SELECT jsonb_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToOne
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i) WHERE (("products"."id") = '{{id}}' :: bigint) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT CAST( i.j ->>'email' AS character varying) FROM "_sg_input" i) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToManyWithConnect
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT CAST( i.j ->>'full_name' AS character varying), CAST( i.j ->>'email' AS character varying), CAST( i.j ->>'created_at' AS timestamp without time zone), CAST( i.j ->>'updated_at' AS timestamp without time zone) FROM "_sg_input" i) WHERE (("users"."id") = '{{id}}' :: bigint) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id"= ((i.j->'product'->'disconnect'->>'id'))::bigint) RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") SELECT jsonb_build_object('user', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "__sj_1"."json" AS "product" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToOneWithConnect
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint AND "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), "_x_users"."id" FROM "_sg_input" i", "_x_users) WHERE (("products"."id") = '{{product_id}}' :: bigint) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying AND "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), "_x_users"."id" FROM "_sg_input" i", "_x_users) WHERE (("products"."id") = '{{product_id}}' :: bigint) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "__sj_1"."json" AS "user" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT to_jsonb("__sr_1".*) AS "json"FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "__sr_1") AS "__sj_1" ON ('true')) AS "__sr_0") AS "__sj_0"
|
||||
=== RUN TestCompileUpdate/nestedUpdateOneToOneWithDisconnect
|
||||
WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT CAST( i.j ->>'name' AS character varying), CAST( i.j ->>'price' AS numeric(7,2)), "_x_users"."id" FROM "_sg_input" i", "_x_users) WHERE (("products"."id") = '{{id}}' :: bigint) RETURNING "products".*) SELECT jsonb_build_object('product', "__sj_0"."json") as "__root" FROM (SELECT to_jsonb("__sr_0".*) AS "json"FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "__sr_0") AS "__sj_0"
|
||||
--- PASS: TestCompileUpdate (0.04s)
|
||||
--- PASS: TestCompileUpdate/singleUpdate (0.00s)
|
||||
--- PASS: TestCompileUpdate/simpleUpdateWithPresets (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateManyToMany (0.01s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToMany (0.01s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToOne (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToManyWithConnect (0.00s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToOneWithConnect (0.01s)
|
||||
--- PASS: TestCompileUpdate/nestedUpdateOneToOneWithDisconnect (0.00s)
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/core/internal/psql 0.154s
|
||||
=== RUN TestCompile1
|
||||
--- PASS: TestCompile1 (0.00s)
|
||||
=== RUN TestCompile2
|
||||
--- PASS: TestCompile2 (0.00s)
|
||||
=== RUN TestCompile3
|
||||
--- PASS: TestCompile3 (0.00s)
|
||||
=== RUN TestInvalidCompile1
|
||||
--- PASS: TestInvalidCompile1 (0.00s)
|
||||
=== RUN TestInvalidCompile2
|
||||
--- PASS: TestInvalidCompile2 (0.00s)
|
||||
=== RUN TestEmptyCompile
|
||||
--- PASS: TestEmptyCompile (0.00s)
|
||||
=== RUN TestInvalidPostfixCompile
|
||||
--- PASS: TestInvalidPostfixCompile (0.00s)
|
||||
=== RUN TestGetQType
|
||||
=== RUN TestGetQType/query
|
||||
=== RUN TestGetQType/mutation
|
||||
=== RUN TestGetQType/default_query
|
||||
=== RUN TestGetQType/default_query_with_comment
|
||||
=== RUN TestGetQType/failed_query_with_comment
|
||||
--- PASS: TestGetQType (0.00s)
|
||||
--- PASS: TestGetQType/query (0.00s)
|
||||
--- PASS: TestGetQType/mutation (0.00s)
|
||||
--- PASS: TestGetQType/default_query (0.00s)
|
||||
--- PASS: TestGetQType/default_query_with_comment (0.00s)
|
||||
--- PASS: TestGetQType/failed_query_with_comment (0.00s)
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/core/internal/qcode 0.012s
|
||||
? github.com/dosco/super-graph/core/internal/util [no test files]
|
||||
=== RUN TestFuzzCrashers
|
||||
--- PASS: TestFuzzCrashers (0.00s)
|
||||
=== RUN TestGQLHash1
|
||||
--- PASS: TestGQLHash1 (0.00s)
|
||||
=== RUN TestGQLHash2
|
||||
--- PASS: TestGQLHash2 (0.00s)
|
||||
=== RUN TestGQLHash3
|
||||
--- PASS: TestGQLHash3 (0.00s)
|
||||
=== RUN TestGQLHash4
|
||||
--- PASS: TestGQLHash4 (0.00s)
|
||||
=== RUN TestGQLHashWithVars1
|
||||
--- PASS: TestGQLHashWithVars1 (0.00s)
|
||||
=== RUN TestGQLHashWithVars2
|
||||
--- PASS: TestGQLHashWithVars2 (0.00s)
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/internal/serv 0.033s
|
||||
? github.com/dosco/super-graph/internal/serv/internal/auth [no test files]
|
||||
testing: warning: no tests to run
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/internal/serv/internal/migrate 0.011s [no tests to run]
|
||||
=== RUN TestRailsEncryptedSession1
|
||||
--- PASS: TestRailsEncryptedSession1 (0.01s)
|
||||
=== RUN TestRailsEncryptedSession52
|
||||
--- PASS: TestRailsEncryptedSession52 (0.00s)
|
||||
=== RUN TestRailsJsonSession
|
||||
--- PASS: TestRailsJsonSession (0.00s)
|
||||
=== RUN TestRailsMarshaledSession
|
||||
--- PASS: TestRailsMarshaledSession (0.00s)
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/internal/serv/internal/rails 0.023s
|
||||
=== RUN TestFuzzCrashers
|
||||
--- PASS: TestFuzzCrashers (0.00s)
|
||||
=== RUN TestGet
|
||||
--- PASS: TestGet (0.00s)
|
||||
=== RUN TestGet1
|
||||
--- PASS: TestGet1 (0.00s)
|
||||
=== RUN TestGet2
|
||||
--- PASS: TestGet2 (0.00s)
|
||||
=== RUN TestGet3
|
||||
--- PASS: TestGet3 (0.00s)
|
||||
=== RUN TestGet4
|
||||
--- PASS: TestGet4 (0.00s)
|
||||
=== RUN TestValue
|
||||
--- PASS: TestValue (0.00s)
|
||||
=== RUN TestFilter1
|
||||
--- PASS: TestFilter1 (0.00s)
|
||||
=== RUN TestFilter2
|
||||
--- PASS: TestFilter2 (0.00s)
|
||||
=== RUN TestStrip
|
||||
--- PASS: TestStrip (0.00s)
|
||||
=== RUN TestValidateTrue
|
||||
--- PASS: TestValidateTrue (0.00s)
|
||||
=== RUN TestValidateFalse
|
||||
--- PASS: TestValidateFalse (0.00s)
|
||||
=== RUN TestReplace
|
||||
--- PASS: TestReplace (0.00s)
|
||||
=== RUN TestReplaceEmpty
|
||||
--- PASS: TestReplaceEmpty (0.00s)
|
||||
=== RUN TestKeys1
|
||||
--- PASS: TestKeys1 (0.00s)
|
||||
=== RUN TestKeys2
|
||||
--- PASS: TestKeys2 (0.00s)
|
||||
=== RUN TestKeys3
|
||||
--- PASS: TestKeys3 (0.00s)
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/jsn 0.014s
|
@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/util"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
"github.com/dosco/super-graph/core/internal/util"
|
||||
)
|
||||
|
||||
func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer,
|
||||
@ -91,25 +91,9 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re
|
||||
renderInsertUpdateColumns(w, qc, jt, ti, sk, true)
|
||||
renderNestedUpdateRelColumns(w, item.kvitem, true)
|
||||
|
||||
io.WriteString(w, ` FROM "_sg_input" i, `)
|
||||
io.WriteString(w, ` FROM "_sg_input" i`)
|
||||
renderNestedUpdateRelTables(w, item.kvitem)
|
||||
|
||||
if item.array {
|
||||
io.WriteString(w, `json_populate_recordset`)
|
||||
} else {
|
||||
io.WriteString(w, `json_populate_record`)
|
||||
}
|
||||
|
||||
io.WriteString(w, `(NULL::`)
|
||||
io.WriteString(w, ti.Name)
|
||||
|
||||
if len(item.path) == 0 {
|
||||
io.WriteString(w, `, i.j) t)`)
|
||||
} else {
|
||||
io.WriteString(w, `, i.j->`)
|
||||
joinPath(w, item.path)
|
||||
io.WriteString(w, `) t) `)
|
||||
}
|
||||
io.WriteString(w, `) `)
|
||||
|
||||
if item.id != 0 {
|
||||
// Render sql to set id values if child-to-parent
|
||||
@ -137,9 +121,11 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re
|
||||
io.WriteString(w, `)`)
|
||||
|
||||
} else {
|
||||
io.WriteString(w, ` WHERE `)
|
||||
if err := c.renderWhere(&qc.Selects[0], ti); err != nil {
|
||||
return err
|
||||
if qc.Selects[0].Where != nil {
|
||||
io.WriteString(w, ` WHERE `)
|
||||
if err := c.renderWhere(&qc.Selects[0], ti); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,9 +188,9 @@ func renderNestedUpdateRelTables(w io.Writer, item kvitem) error {
|
||||
// relationship is one-to-many
|
||||
for _, v := range item.items {
|
||||
if v._ctype > 0 && v.relCP.Type == RelOneToMany {
|
||||
io.WriteString(w, `", `)
|
||||
io.WriteString(w, `"_x_`)
|
||||
io.WriteString(w, v.relCP.Left.Table)
|
||||
io.WriteString(w, `", `)
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,6 +208,10 @@ func (c *compilerContext) renderDelete(qc *qcode.QCode, w io.Writer,
|
||||
quoted(c.w, ti.Name)
|
||||
io.WriteString(c.w, ` WHERE `)
|
||||
|
||||
if root.Where == nil {
|
||||
return 0, errors.New("'where' clause missing in delete mutation")
|
||||
}
|
||||
|
||||
if err := c.renderWhere(root, ti); err != nil {
|
||||
return 0, err
|
||||
}
|
@ -6,7 +6,7 @@ import (
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dosco/super-graph/util"
|
||||
"github.com/dosco/super-graph/core/internal/util"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -16,8 +16,8 @@ var (
|
||||
type parserType int32
|
||||
|
||||
const (
|
||||
maxFields = 100
|
||||
maxArgs = 10
|
||||
maxFields = 1200
|
||||
maxArgs = 25
|
||||
)
|
||||
|
||||
const (
|
||||
@ -242,7 +242,8 @@ func (p *Parser) parseOp() (*Operation, error) {
|
||||
|
||||
if p.peek(itemArgsOpen) {
|
||||
p.ignore()
|
||||
op.Args, err = p.parseArgs(op.Args)
|
||||
|
||||
op.Args, err = p.parseOpParams(op.Args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -338,6 +339,13 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) {
|
||||
if p.peek(itemObjOpen) {
|
||||
p.ignore()
|
||||
st.Push(f.ID)
|
||||
|
||||
} else if p.peek(itemObjClose) {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,6 +379,22 @@ func (p *Parser) parseField(f *Field) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseOpParams(args []Arg) ([]Arg, error) {
|
||||
for {
|
||||
if len(args) >= maxArgs {
|
||||
return nil, fmt.Errorf("too many args (max %d)", maxArgs)
|
||||
}
|
||||
|
||||
if p.peek(itemArgsClose) {
|
||||
p.ignore()
|
||||
break
|
||||
}
|
||||
p.next()
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
|
||||
var err error
|
||||
|
||||
@ -383,6 +407,7 @@ func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
|
||||
p.ignore()
|
||||
break
|
||||
}
|
||||
|
||||
if !p.peek(itemName) {
|
||||
return nil, errors.New("expecting an argument name")
|
||||
}
|
@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dosco/super-graph/util"
|
||||
"github.com/dosco/super-graph/core/internal/util"
|
||||
"github.com/gobuffalo/flect"
|
||||
)
|
||||
|
47
core/internal/qcode/utils.go
Normal file
@ -0,0 +1,47 @@
|
||||
package qcode
|
||||
|
||||
func GetQType(gql string) QType {
|
||||
ic := false
|
||||
for i := range gql {
|
||||
b := gql[i]
|
||||
switch {
|
||||
case b == '#':
|
||||
ic = true
|
||||
case b == '\n':
|
||||
ic = false
|
||||
case !ic && b == '{':
|
||||
return QTQuery
|
||||
case !ic && 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')
|
||||
}
|
||||
|
||||
func (qt QType) String() string {
|
||||
switch qt {
|
||||
case QTQuery:
|
||||
return "query"
|
||||
case QTMutation:
|
||||
return "mutation"
|
||||
case QTInsert:
|
||||
return "insert"
|
||||
case QTUpdate:
|
||||
return "update"
|
||||
case QTDelete:
|
||||
return "delete"
|
||||
case QTUpsert:
|
||||
return "upsert"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
50
core/internal/qcode/utils_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package qcode
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetQType(t *testing.T) {
|
||||
type args struct {
|
||||
gql string
|
||||
}
|
||||
type ts struct {
|
||||
name string
|
||||
args args
|
||||
want QType
|
||||
}
|
||||
tests := []ts{
|
||||
ts{
|
||||
name: "query",
|
||||
args: args{gql: " query {"},
|
||||
want: QTQuery,
|
||||
},
|
||||
ts{
|
||||
name: "mutation",
|
||||
args: args{gql: " mutation {"},
|
||||
want: QTMutation,
|
||||
},
|
||||
ts{
|
||||
name: "default query",
|
||||
args: args{gql: " {"},
|
||||
want: QTQuery,
|
||||
},
|
||||
ts{
|
||||
name: "default query with comment",
|
||||
args: args{gql: `# query is good
|
||||
{`},
|
||||
want: QTQuery,
|
||||
},
|
||||
ts{
|
||||
name: "failed query with comment",
|
||||
args: args{gql: `# query is good query {`},
|
||||
want: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetQType(tt.args.gql); got != tt.want {
|
||||
t.Errorf("GetQType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
277
core/prepare.go
Normal file
@ -0,0 +1,277 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/dosco/super-graph/core/internal/allow"
|
||||
"github.com/dosco/super-graph/core/internal/psql"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
"github.com/valyala/fasttemplate"
|
||||
)
|
||||
|
||||
type preparedItem struct {
|
||||
sd *sql.Stmt
|
||||
args [][]byte
|
||||
st stmt
|
||||
roleArg bool
|
||||
}
|
||||
|
||||
func (sg *SuperGraph) initPrepared() error {
|
||||
ct := context.Background()
|
||||
|
||||
if sg.allowList.IsPersist() {
|
||||
return nil
|
||||
}
|
||||
sg.prepared = make(map[string]*preparedItem)
|
||||
|
||||
tx, err := sg.db.BeginTx(ct, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback() //nolint: errcheck
|
||||
|
||||
if err = sg.prepareRoleStmt(tx); err != nil {
|
||||
return fmt.Errorf("prepareRoleStmt: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
success := 0
|
||||
|
||||
list, err := sg.allowList.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range list {
|
||||
if len(v.Query) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
err := sg.prepareStmt(v)
|
||||
if err == nil {
|
||||
success++
|
||||
continue
|
||||
}
|
||||
|
||||
// if len(v.Vars) == 0 {
|
||||
// logger.Warn().Err(err).Msg(v.Query)
|
||||
// } else {
|
||||
// logger.Warn().Err(err).Msgf("%s %s", v.Vars, v.Query)
|
||||
// }
|
||||
}
|
||||
|
||||
// logger.Info().
|
||||
// Msgf("Registered %d of %d queries from allow.list as prepared statements",
|
||||
// success, len(list))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sg *SuperGraph) prepareStmt(item allow.Item) error {
|
||||
query := item.Query
|
||||
qb := []byte(query)
|
||||
vars := item.Vars
|
||||
|
||||
qt := qcode.GetQType(query)
|
||||
ct := context.Background()
|
||||
|
||||
tx, err := sg.db.BeginTx(ct, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback() //nolint: errcheck
|
||||
|
||||
switch qt {
|
||||
case qcode.QTQuery:
|
||||
var stmts1 []stmt
|
||||
var err error
|
||||
|
||||
if sg.abacEnabled {
|
||||
stmts1, err = sg.buildMultiStmt(qb, vars)
|
||||
} else {
|
||||
stmts1, err = sg.buildRoleStmt(qb, vars, "user")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//logger.Debug().Msgf("Prepared statement 'query %s' (user)", item.Name)
|
||||
|
||||
err = sg.prepare(ct, tx, stmts1, stmtHash(item.Name, "user"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sg.anonExists {
|
||||
// logger.Debug().Msgf("Prepared statement 'query %s' (anon)", item.Name)
|
||||
|
||||
stmts2, err := sg.buildRoleStmt(qb, vars, "anon")
|
||||
if err == psql.ErrAllTablesSkipped {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sg.prepare(ct, tx, stmts2, stmtHash(item.Name, "anon"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case qcode.QTMutation:
|
||||
for _, role := range sg.conf.Roles {
|
||||
// logger.Debug().Msgf("Prepared statement 'mutation %s' (%s)", item.Name, role.Name)
|
||||
|
||||
stmts, err := sg.buildRoleStmt(qb, vars, role.Name)
|
||||
|
||||
if err != nil {
|
||||
// if len(item.Vars) == 0 {
|
||||
// logger.Warn().Err(err).Msg(item.Query)
|
||||
// } else {
|
||||
// logger.Warn().Err(err).Msgf("%s %s", item.Vars, item.Query)
|
||||
// }
|
||||
continue
|
||||
}
|
||||
|
||||
err = sg.prepare(ct, tx, stmts, stmtHash(item.Name, role.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sg *SuperGraph) prepare(ct context.Context, tx *sql.Tx, st []stmt, key string) error {
|
||||
finalSQL, am := processTemplate(st[0].sql)
|
||||
|
||||
sd, err := tx.Prepare(finalSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sg.prepared[key] = &preparedItem{
|
||||
sd: sd,
|
||||
args: am,
|
||||
st: st[0],
|
||||
roleArg: len(st) > 1,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
func (sg *SuperGraph) prepareRoleStmt(tx *sql.Tx) error {
|
||||
var err error
|
||||
|
||||
if !sg.abacEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
io.WriteString(w, `SELECT (CASE WHEN EXISTS (`)
|
||||
io.WriteString(w, sg.conf.RolesQuery)
|
||||
io.WriteString(w, `) THEN `)
|
||||
|
||||
io.WriteString(w, `(SELECT (CASE`)
|
||||
for _, role := range sg.conf.Roles {
|
||||
if len(role.Match) == 0 {
|
||||
continue
|
||||
}
|
||||
io.WriteString(w, ` WHEN `)
|
||||
io.WriteString(w, role.Match)
|
||||
io.WriteString(w, ` THEN '`)
|
||||
io.WriteString(w, role.Name)
|
||||
io.WriteString(w, `'`)
|
||||
}
|
||||
|
||||
io.WriteString(w, ` ELSE {{role}} END) FROM (`)
|
||||
io.WriteString(w, sg.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" LIMIT 1; `)
|
||||
|
||||
roleSQL, _ := processTemplate(w.String())
|
||||
|
||||
sg.getRole, err = tx.Prepare(roleSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processTemplate(tmpl string) (string, [][]byte) {
|
||||
st := struct {
|
||||
vmap map[string]int
|
||||
am [][]byte
|
||||
i int
|
||||
}{
|
||||
vmap: make(map[string]int),
|
||||
am: make([][]byte, 0, 5),
|
||||
i: 0,
|
||||
}
|
||||
|
||||
execFunc := func(w io.Writer, tag string) (int, error) {
|
||||
if n, ok := st.vmap[tag]; ok {
|
||||
return w.Write([]byte(fmt.Sprintf("$%d", n)))
|
||||
}
|
||||
st.am = append(st.am, []byte(tag))
|
||||
st.i++
|
||||
st.vmap[tag] = st.i
|
||||
return w.Write([]byte(fmt.Sprintf("$%d", st.i)))
|
||||
}
|
||||
|
||||
t1 := fasttemplate.New(tmpl, `'{{`, `}}'`)
|
||||
ts1 := t1.ExecuteFuncString(execFunc)
|
||||
|
||||
t2 := fasttemplate.New(ts1, `{{`, `}}`)
|
||||
ts2 := t2.ExecuteFuncString(execFunc)
|
||||
|
||||
return ts2, st.am
|
||||
}
|
||||
|
||||
func (sg *SuperGraph) initAllowList() error {
|
||||
var ac allow.Config
|
||||
var err error
|
||||
|
||||
if len(sg.conf.AllowListFile) == 0 {
|
||||
sg.conf.UseAllowList = false
|
||||
sg.log.Printf("WRN allow list disabled no file specified")
|
||||
}
|
||||
|
||||
if !sg.conf.UseAllowList {
|
||||
ac = allow.Config{CreateIfNotExists: true, Persist: true}
|
||||
}
|
||||
|
||||
sg.allowList, err = allow.New(sg.conf.AllowListFile, ac)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize allow list: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
func stmtHash(name string, role string) string {
|
||||
h := sha1.New()
|
||||
io.WriteString(h, strings.ToLower(name))
|
||||
io.WriteString(h, role)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package serv
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -8,24 +8,20 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/dosco/super-graph/core/internal/qcode"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
)
|
||||
|
||||
func execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([]byte, error) {
|
||||
func (sg *SuperGraph) 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)
|
||||
fids, sfmap := sg.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
|
||||
@ -34,10 +30,10 @@ func execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([]byte, error) {
|
||||
|
||||
switch {
|
||||
case len(from) == 1:
|
||||
to, err = resolveRemote(hdr, h, from[0], sel, sfmap)
|
||||
to, err = sg.resolveRemote(hdr, h, from[0], sel, sfmap)
|
||||
|
||||
case len(from) > 1:
|
||||
to, err = resolveRemotes(hdr, h, from, sel, sfmap)
|
||||
to, err = sg.resolveRemotes(hdr, h, from, sel, sfmap)
|
||||
|
||||
default:
|
||||
return nil, errors.New("something wrong no remote ids found in db response")
|
||||
@ -57,7 +53,7 @@ func execRemoteJoin(st *stmt, data []byte, hdr http.Header) ([]byte, error) {
|
||||
return ob.Bytes(), nil
|
||||
}
|
||||
|
||||
func resolveRemote(
|
||||
func (sg *SuperGraph) resolveRemote(
|
||||
hdr http.Header,
|
||||
h *xxhash.Digest,
|
||||
field jsn.Field,
|
||||
@ -82,7 +78,7 @@ func resolveRemote(
|
||||
// to find the resolver to use for this relationship
|
||||
k2 := mkkey(h, s.Name, p.Name)
|
||||
|
||||
r, ok := rmap[k2]
|
||||
r, ok := sg.rmap[k2]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
@ -119,7 +115,7 @@ func resolveRemote(
|
||||
return to, nil
|
||||
}
|
||||
|
||||
func resolveRemotes(
|
||||
func (sg *SuperGraph) resolveRemotes(
|
||||
hdr http.Header,
|
||||
h *xxhash.Digest,
|
||||
from []jsn.Field,
|
||||
@ -150,7 +146,7 @@ func resolveRemotes(
|
||||
// to find the resolver to use for this relationship
|
||||
k2 := mkkey(h, s.Name, p.Name)
|
||||
|
||||
r, ok := rmap[k2]
|
||||
r, ok := sg.rmap[k2]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
@ -195,3 +191,59 @@ func resolveRemotes(
|
||||
|
||||
return to, cerr
|
||||
}
|
||||
|
||||
func (sg *SuperGraph) parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) (
|
||||
[][]byte,
|
||||
map[uint64]*qcode.Select) {
|
||||
|
||||
c := 0
|
||||
for i := range sel {
|
||||
s := &sel[i]
|
||||
if isSkipped(skipped, uint32(s.ID)) {
|
||||
c++
|
||||
}
|
||||
}
|
||||
|
||||
// list of keys (and it's related value) to extract from
|
||||
// the db json response
|
||||
fm := make([][]byte, c)
|
||||
|
||||
// mapping between the above extracted key and a Select
|
||||
// object
|
||||
sm := make(map[uint64]*qcode.Select, c)
|
||||
n := 0
|
||||
|
||||
for i := range sel {
|
||||
s := &sel[i]
|
||||
|
||||
if !isSkipped(skipped, uint32(s.ID)) {
|
||||
continue
|
||||
}
|
||||
|
||||
p := sel[s.ParentID]
|
||||
k := mkkey(h, s.Name, p.Name)
|
||||
|
||||
if r, ok := sg.rmap[k]; ok {
|
||||
fm[n] = r.IDField
|
||||
n++
|
||||
|
||||
k := xxhash.Sum64(r.IDField)
|
||||
sm[k] = s
|
||||
}
|
||||
}
|
||||
|
||||
return fm, sm
|
||||
}
|
||||
|
||||
func isSkipped(n uint32, pos uint32) bool {
|
||||
return ((n & (1 << pos)) != 0)
|
||||
}
|
||||
|
||||
func colsToList(cols []qcode.Column) []string {
|
||||
var f []string
|
||||
|
||||
for i := range cols {
|
||||
f = append(f, cols[i].Name)
|
||||
}
|
||||
return f
|
||||
}
|
@ -1,19 +1,14 @@
|
||||
package serv
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/dosco/super-graph/core/internal/psql"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
"github.com/dosco/super-graph/psql"
|
||||
)
|
||||
|
||||
var (
|
||||
rmap map[uint64]*resolvFn
|
||||
)
|
||||
|
||||
type resolvFn struct {
|
||||
@ -22,23 +17,25 @@ type resolvFn struct {
|
||||
Fn func(h http.Header, id []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
func initResolvers() {
|
||||
func (sg *SuperGraph) initResolvers() error {
|
||||
var err error
|
||||
rmap = make(map[uint64]*resolvFn)
|
||||
sg.rmap = make(map[uint64]*resolvFn)
|
||||
|
||||
for _, t := range conf.Tables {
|
||||
err = initRemotes(t)
|
||||
for _, t := range sg.conf.Tables {
|
||||
err = sg.initRemotes(t)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to initialize resolvers")
|
||||
return fmt.Errorf("failed to initialize resolvers: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initRemotes(t configTable) error {
|
||||
func (sg *SuperGraph) initRemotes(t Table) error {
|
||||
h := xxhash.New()
|
||||
|
||||
for _, r := range t.Remotes {
|
||||
@ -49,7 +46,7 @@ func initRemotes(t configTable) error {
|
||||
// if no table column specified in the config then
|
||||
// use the primary key of the table as the id
|
||||
if len(idcol) == 0 {
|
||||
pcol, err := pcompile.IDColumn(t.Name)
|
||||
pcol, err := sg.pc.IDColumn(t.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -64,7 +61,7 @@ func initRemotes(t configTable) error {
|
||||
val.Left.Col = idcol
|
||||
val.Right.Col = idk
|
||||
|
||||
err := pcompile.AddRelationship(strings.ToLower(r.Name), t.Name, val)
|
||||
err := sg.pc.AddRelationship(sanitize(r.Name), t.Name, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -85,16 +82,16 @@ func initRemotes(t configTable) error {
|
||||
}
|
||||
|
||||
// index resolver obj by parent and child names
|
||||
rmap[mkkey(h, r.Name, t.Name)] = rf
|
||||
sg.rmap[mkkey(h, r.Name, t.Name)] = rf
|
||||
|
||||
// index resolver obj by IDField
|
||||
rmap[xxhash.Sum64(rf.IDField)] = rf
|
||||
sg.rmap[xxhash.Sum64(rf.IDField)] = rf
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildFn(r configRemote) func(http.Header, []byte) ([]byte, error) {
|
||||
func buildFn(r Remote) func(http.Header, []byte) ([]byte, error) {
|
||||
reqURL := strings.Replace(r.URL, "$id", "%s", 1)
|
||||
client := &http.Client{}
|
||||
|
||||
@ -117,28 +114,25 @@ func buildFn(r configRemote) func(http.Header, []byte) ([]byte, error) {
|
||||
req.Header.Set(v, hdr.Get(v))
|
||||
}
|
||||
|
||||
logger.Debug().Str("uri", uri).Msg("Remote Join")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
errlog.Error().Err(err).Msgf("Failed to connect to: %s", uri)
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to connect to '%s': %v", uri, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if r.Debug {
|
||||
reqDump, err := httputil.DumpRequestOut(req, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// reqDump, err := httputil.DumpRequestOut(req, true)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
resDump, err := httputil.DumpResponse(res, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// resDump, err := httputil.DumpResponse(res, true)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
logger.Debug().Msgf("Remote Request Debug:\n%s\n%s",
|
||||
reqDump, resDump)
|
||||
// logger.Debug().Msgf("Remote Request Debug:\n%s\n%s",
|
||||
// reqDump, resDump)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
15
core/utils.go
Normal file
@ -0,0 +1,15 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/cespare/xxhash/v2"
|
||||
)
|
||||
|
||||
// nolint: errcheck
|
||||
func mkkey(h *xxhash.Digest, k1 string, k2 string) uint64 {
|
||||
h.WriteString(k1)
|
||||
h.WriteString(k2)
|
||||
v := h.Sum64()
|
||||
h.Reset()
|
||||
|
||||
return v
|
||||
}
|
@ -1,267 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<main aria-labelledby="main-title" >
|
||||
<Navbar />
|
||||
|
||||
<div class="container mx-auto">
|
||||
<div class="flex flex-col md:flex-row justify-between px-10 md:px-20">
|
||||
<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">
|
||||
<img src="/super-graph.png" width="250" />
|
||||
</h1>
|
||||
|
||||
<p class="text-4xl text-gray-800 leading-tight mt-1">
|
||||
Build web products faster. Secure high performance GraphQL
|
||||
</p>
|
||||
|
||||
<NavLink
|
||||
class="inline-block px-4 py-3 my-8 bg-blue-600 text-blue-100 font-bold rounded"
|
||||
:item="actionLink"
|
||||
/>
|
||||
|
||||
<a
|
||||
class="px-4 py-3 my-8 border-2 border-gray-500 text-gray-600 font-bold rounded"
|
||||
href="https://github.com/dosco/super-graph"
|
||||
target="_blank"
|
||||
>Github</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-10 md:p-20">
|
||||
<img src="/hologram.svg" class="h-64">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="flex flex-wrap mx-2 md:mx-20"
|
||||
v-if="data.features && data.features.length"
|
||||
>
|
||||
<div
|
||||
class="w-2/4 md:w-1/3 shadow"
|
||||
v-for="(feature, index) in data.features"
|
||||
:key="index"
|
||||
>
|
||||
<div class="p-8">
|
||||
<h2 class="md:text-xl text-blue-800 font-medium border-0 mb-1">{{ feature.title }}</h2>
|
||||
<p class="md:text-xl text-gray-700 leading-snug">{{ feature.details }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-gray-100 mt-10">
|
||||
<div class="container mx-auto px-10 md:px-0 py-32">
|
||||
|
||||
<div class="pb-8 hidden md:block ">
|
||||
<img src="arch-basic.svg">
|
||||
</div>
|
||||
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
What is {{ data.heroText }}?
|
||||
</h1>
|
||||
<div class="text-2xl md:text-3xl">
|
||||
Super Graph can automatically learn a Postgres database and instantly serve it as a fast and secured GraphQL API. It comes with tools to create a new app and manage it's database. You get it all, a very productive developer and a highly scalable app backend. It's designed to work well on serverless platforms by Google, AWS, Microsoft, etc. The goal is to save you a ton of time and money so you can focus on you're apps core value.
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
<div class="md:w-2/4">
|
||||
<img src="/graphql.png">
|
||||
</div>
|
||||
|
||||
<div class="md:w-2/4">
|
||||
<img src="/json.png">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 py-10 md:py-20">
|
||||
<div class="container mx-auto px-10 md:px-0">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
How to use {{ data.heroText }}?
|
||||
</h1>
|
||||
<div class="text-2xl md:text-3xl">
|
||||
<small class="text-sm">Use the below command to download and install Super Graph. You will need Go 1.13 or above</small>
|
||||
<pre>‣ GO111MODULE=on go get -u github.com/dosco/super-graph</pre>
|
||||
|
||||
<small class="text-sm">Create a new app and change to it's directory</small>
|
||||
<pre>‣ super-graph new blog; cd blog</pre>
|
||||
|
||||
<small class="text-sm">Setup the app database and seed it with fake data. Docker compose will start a Postgres database for your app</small>
|
||||
<pre>‣ docker-compose run blog_api ./super-graph db:setup</pre>
|
||||
|
||||
<small class="text-sm">And finally launch Super Graph configured for your app</small>
|
||||
<pre>‣ docker-compose up</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-100 mt-10">
|
||||
<div class="container mx-auto px-10 md:px-0 py-32">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
The story of {{ data.heroText }}
|
||||
</h1>
|
||||
<div class="text-2xl md:text-3xl">
|
||||
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.<br><br>
|
||||
|
||||
It's always the same thing, figure out what the UI needs then build an endpoint for it. Most API code involves struggling with an ORM to query a database and mangle the data into a shape that the UI expects to see.<br><br>
|
||||
|
||||
I didn't want to write this code anymore, I wanted the computer to do it. Enter GraphQL, to me it sounded great, but it still required me to write all the same database query code.<br><br>
|
||||
|
||||
Having worked with compilers before I saw this as a compiler problem. Why not build a compiler that converts GraphQL to highly efficient SQL.<br><br>
|
||||
|
||||
This compiler is what sits at the heart of Super Graph with layers of useful functionality around it like authentication, remote joins, rails integration, database migrations and everything else needed for you to build production ready apps with it.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden bg-indigo-900">
|
||||
<div class="container mx-auto py-20">
|
||||
<img src="/super-graph-web-ui.png">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-10 md:py-20">
|
||||
<div class="container mx-auto px-10 md:px-0">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
Try it with a demo Rails app
|
||||
</h1>
|
||||
<div class="text-2xl md:text-3xl">
|
||||
<small class="text-sm">Download the Docker compose config for the demo</small>
|
||||
<pre>‣ curl -L -o demo.yml https://bit.ly/2FZS0uw</pre>
|
||||
|
||||
<small class="text-sm">Setup the demo database</small>
|
||||
<pre>‣ docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed</pre>
|
||||
|
||||
<small class="text-sm">Run the demo</small>
|
||||
<pre>‣ docker-compose -f demo.yml up</pre>
|
||||
|
||||
<small class="text-sm">Signin to the demo app (user1@demo.com / 123456)</small>
|
||||
<pre>‣ open http://localhost:3000</pre>
|
||||
|
||||
<small class="text-sm">Try the super graph web ui</small>
|
||||
<pre>‣ open http://localhost:8080</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t py-10">
|
||||
<div class="block md:hidden w-100">
|
||||
<iframe src='https://www.youtube.com/embed/MfPL2A-DAJk' frameborder='0' allowfullscreen style="width: 100%; height: 250px;">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto flex flex-col md:flex-row items-center">
|
||||
<div class="w-100 md:w-1/2 p-8">
|
||||
<h1 class="text-2xl font-bold">GraphQL the future of APIs</h1>
|
||||
<p class="text-xl text-gray-600">Keeping a tight and fast development loop helps you iterate quickly. Leveraging technology like Super Graph focuses your team on building the core product and not reinventing wheels. GraphQL eliminate the dependency on the backend engineering and keeps the things moving fast</p>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:block md:w-1/2">
|
||||
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style>
|
||||
<div class="embed-container shadow">
|
||||
<iframe src='https://www.youtube.com/embed/MfPL2A-DAJk' frameborder='0' allowfullscreen >
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-200 mt-10">
|
||||
<div class="container mx-auto px-10 md:px-0 py-32">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
Build Secure Apps
|
||||
</h1>
|
||||
<div class="flex flex-col text-2xl md:text-3xl">
|
||||
<card className="mb-1 p-8">
|
||||
<template #image><font-awesome-icon icon="portrait" class="text-red-500" /></template>
|
||||
<template #title>Role Based Access Control</template>
|
||||
<template #body>Dynamically assign roles like admin, manager or anon to specific users. Generate role specific queries at runtime. For example admins can get all users while others can only fetch their own user.</template>
|
||||
</card>
|
||||
<card className="mb-1 p-8">
|
||||
<template #image><font-awesome-icon icon="shield-alt" class="text-blue-500" /></template>
|
||||
<template #title>Prepared Statements</template>
|
||||
<template #body>An additional layer of protection from a variety of security issues like SQL injection. In production mode all queries are precompiled into prepared statements so only those can be executed. This also significantly speeds up all queries.</template>
|
||||
</card>
|
||||
<card className="p-8">
|
||||
<template #image><font-awesome-icon icon="lock" class="text-green-500"/></template>
|
||||
<template #title>Fuzz Tested Code</template>
|
||||
<template #body>Fuzzing is done by complex software that generates massives amounts of random input to detect if code is free of security bugs. Google uses fuzzing to protects everything from their cloud infrastructure to the Chrome browser.</template>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<div class="container mx-auto px-10 md:px-0 py-32">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
More Features
|
||||
</h1>
|
||||
<div class="flex flex-col md:flex-row text-2xl md:text-3xl">
|
||||
<card className="mr-0 md:mr-1 mb-1 flex-col w-100 md:w-1/3 items-center">
|
||||
<template #image><img src="/arch-remote-join.svg" class="h-64"></template>
|
||||
<template #title>Remote Joins</template>
|
||||
<template #body>A powerful feature that allows you to query your database and remote REST APIs at the same time. For example fetch a user from the DB, his tweets from Twitter and his payments from Stripe with a single GraphQL query.</template>
|
||||
</card>
|
||||
<card className="mr-0 md:mr-1 mb-1 flex-col w-100 md:w-1/3">
|
||||
<template #image><img src="/arch-search.svg" class="h-64"></template>
|
||||
<template #title>Full Text Search</template>
|
||||
<template #body>Postgres has excellent full-text search built-in. You don't need another expensive service. Super Graph makes it super easy to use with keyword ranking and highlighting also supported.</template>
|
||||
</card>
|
||||
<card className="mb-1 flex-col w-100 md:w-1/3">
|
||||
<template #image><img src="/arch-bulk.svg" class="h-64"></template>
|
||||
<template #title>Bulk Inserts</template>
|
||||
<template #body>Efficiently insert, update and delete multiple items with a single query. Upserts are also supported</template>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mx-auto text-center py-8"
|
||||
v-if="data.footer"
|
||||
>
|
||||
{{ data.footer }}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavLink from '@theme/components/NavLink.vue'
|
||||
import Navbar from '@theme/components/Navbar.vue'
|
||||
import Card from './Card.vue'
|
||||
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faPortrait, faShieldAlt, faLock } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
library.add(faPortrait, faShieldAlt, faLock)
|
||||
|
||||
export default {
|
||||
components: { NavLink, Navbar, FontAwesomeIcon, Card },
|
||||
|
||||
computed: {
|
||||
data () {
|
||||
return this.$page.frontmatter
|
||||
},
|
||||
|
||||
actionLink () {
|
||||
return {
|
||||
link: this.data.actionLink,
|
||||
text: this.data.actionText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
0
docs/.gitignore → docs/guide/.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="shadow bg-white p-4 flex items-start" :class="className">
|
||||
<div class="shadow p-4 flex items-start" :class="className">
|
||||
<slot name="image"></slot>
|
||||
<div class="pl-4">
|
||||
<h2 class="p-0">
|
436
docs/guide/.vuepress/components/HomeLayout.vue
Normal file
@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<div>
|
||||
<main aria-labelledby="main-title" >
|
||||
<Navbar />
|
||||
<div style="height: 3.6rem"></div>
|
||||
|
||||
<div class="container mx-auto pt-4">
|
||||
<div class="text-center">
|
||||
<div class="text-center text-3xl md:text-4xl text-black leading-tight font-semibold">
|
||||
Fetch data without code
|
||||
</div>
|
||||
|
||||
<NavLink
|
||||
class="inline-block px-4 py-3 my-8 bg-blue-600 text-white font-bold rounded"
|
||||
:item="actionLink"
|
||||
/>
|
||||
|
||||
<a
|
||||
class="px-4 py-3 my-8 border-2 border-blue-600 text-blue-600 font-bold rounded"
|
||||
href="https://github.com/dosco/super-graph"
|
||||
target="_blank"
|
||||
>Github</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="container mx-auto mb-8 mt-0 md:mt-20 bg-green-100">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-100 md:w-1/2 border border-green-500 text-gray-6 00 text-sm md:text-lg p-6">
|
||||
<div class="text-xl font-bold pb-4">Before, struggle with SQL</div>
|
||||
<pre>
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Profile Profile
|
||||
ProfileID int
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
}
|
||||
|
||||
db.Model(&user).
|
||||
Related(&profile).
|
||||
Association("Languages").
|
||||
Where("name in (?)", []string{"test"}).
|
||||
Joins("left join emails on emails.user_id = users.id")
|
||||
Find(&users)
|
||||
|
||||
and more ...
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="w-100 md:w-1/2 border border-l md:border-l-0 border-green-500 text-blue-900 text-sm md:text-lg p-6">
|
||||
<div class="text-xl font-bold pb-4">With Super Graph, just ask.</div>
|
||||
<pre>
|
||||
query {
|
||||
user(id: 5) {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
picture_url
|
||||
posts(first: 20, order_by: { score: desc }) {
|
||||
slug
|
||||
title
|
||||
created_at
|
||||
votes_total
|
||||
votes { created_at }
|
||||
author { id name }
|
||||
tags { id name }
|
||||
}
|
||||
posts_cursor
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-0 md:mt-20">
|
||||
<div
|
||||
class="flex flex-wrap mx-2 md:mx-20"
|
||||
v-if="data.features && data.features.length"
|
||||
>
|
||||
<div
|
||||
class="w-2/4 md:w-1/3 shadow"
|
||||
v-for="(feature, index) in data.features"
|
||||
:key="index"
|
||||
>
|
||||
<div class="p-8">
|
||||
<h2 class="text-lg uppercase border-0">{{ feature.title }}</h2>
|
||||
<div class="text-xl text-gray-900 leading-snug">{{ feature.details }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pt-0 md:pt-20">
|
||||
<div class="container mx-auto p-10">
|
||||
|
||||
<div class="flex justify-center pb-20">
|
||||
<img src="arch-basic.svg">
|
||||
</div>
|
||||
|
||||
<div class="text-2xl md:text-3xl">
|
||||
Super Graph is a library and service that fetches data from any Postgres database using just GraphQL. No more struggling with ORMs and SQL to wrangle data out of the database. No more having to figure out the right joins or making ineffiient queries. However complex the GraphQL, Super Graph will always generate just one single efficient SQL query. The goal is to save you time and money so you can focus on you're apps core value.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pt-20">
|
||||
<div class="container mx-auto px-10 md:px-0">
|
||||
<h1 class="uppercase font-semibold text-2xl text-blue-800 text-center">
|
||||
Try Super Graph
|
||||
</h1>
|
||||
|
||||
<h1 class="uppercase font-semibold text-lg text-gray-800">
|
||||
Deploy as a service using docker
|
||||
</h1>
|
||||
<div class="p-4 rounded bg-black text-white">
|
||||
<pre>$ git clone https://github.com/dosco/super-graph && cd super-graph && make install</pre>
|
||||
<pre>$ super-graph new blog; cd blog</pre>
|
||||
<pre>$ docker-compose run blog_api ./super-graph db:setup</pre>
|
||||
<pre>$ docker-compose up</pre>
|
||||
</div>
|
||||
|
||||
<h1 class="uppercase font-semibold text-lg text-gray-800">
|
||||
Or use it with your own code
|
||||
</h1>
|
||||
<div class="text-md">
|
||||
<pre class="p-4 rounded bg-black text-white">
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/dosco/super-graph/config"
|
||||
"github.com/dosco/super-graph/core"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("pgx", "postgres://postgrs:@localhost:5432/example_db")
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
conf, err := config.NewConfig("./config")
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
sg, err = core.NewSuperGraph(conf, db)
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
graphqlQuery := `
|
||||
query {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
}
|
||||
}`
|
||||
|
||||
res, err := sg.GraphQL(context.Background(), graphqlQuery, nil)
|
||||
if err != nil {
|
||||
log.Fatalf(err)
|
||||
}
|
||||
|
||||
fmt.Println(string(res.Data))
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-0 md:pt-20">
|
||||
<div class="container mx-auto px-10 md:px-0 py-32">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
The story of {{ data.heroText }}
|
||||
</h1>
|
||||
<div class="text-2xl md:text-3xl">
|
||||
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.<br><br>
|
||||
|
||||
It's always the same thing, figure out what the UI needs then build an endpoint for it. Most API code involves struggling with an ORM to query a database and mangle the data into a shape that the UI expects to see.<br><br>
|
||||
|
||||
I didn't want to write this code anymore, I wanted the computer to do it. Enter GraphQL, to me it sounded great, but it still required me to write all the same database query code.<br><br>
|
||||
|
||||
Having worked with compilers before I saw this as a compiler problem. Why not build a compiler that converts GraphQL to highly efficient SQL.<br><br>
|
||||
|
||||
This compiler is what sits at the heart of Super Graph with layers of useful functionality around it like authentication, remote joins, rails integration, database migrations and everything else needed for you to build production ready apps with it.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden bg-indigo-900">
|
||||
<div class="container mx-auto py-20">
|
||||
<img src="/super-graph-web-ui.png">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="py-10 md:py-20">
|
||||
<div class="container mx-auto px-10 md:px-0">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
Try it with a demo Rails app
|
||||
</h1>
|
||||
<div class="text-2xl md:text-3xl">
|
||||
<small class="text-sm">Download the Docker compose config for the demo</small>
|
||||
<pre>‣ curl -L -o demo.yml https://bit.ly/2FZS0uw</pre>
|
||||
|
||||
<small class="text-sm">Setup the demo database</small>
|
||||
<pre>‣ docker-compose -f demo.yml run rails_app rake db:create db:migrate db:seed</pre>
|
||||
|
||||
<small class="text-sm">Run the demo</small>
|
||||
<pre>‣ docker-compose -f demo.yml up</pre>
|
||||
|
||||
<small class="text-sm">Signin to the demo app (user1@demo.com / 123456)</small>
|
||||
<pre>‣ open http://localhost:3000</pre>
|
||||
|
||||
<small class="text-sm">Try the super graph web ui</small>
|
||||
<pre>‣ open http://localhost:8080</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="pt-0 md:pt-20">
|
||||
<div class="block md:hidden w-100">
|
||||
<iframe src='https://www.youtube.com/embed/MfPL2A-DAJk' frameborder='0' allowfullscreen style="width: 100%; height: 250px;">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto flex flex-col md:flex-row items-center">
|
||||
<div class="w-100 md:w-1/2 p-8">
|
||||
<h1 class="text-2xl font-bold">GraphQL the future of APIs</h1>
|
||||
<p class="text-xl text-gray-600">Keeping a tight and fast development loop helps you iterate quickly. Leveraging technology like Super Graph focuses your team on building the core product and not reinventing wheels. GraphQL eliminate the dependency on the backend engineering and keeps the things moving fast</p>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:block md:w-1/2">
|
||||
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style>
|
||||
<div class="embed-container shadow">
|
||||
<iframe src='https://www.youtube.com/embed/MfPL2A-DAJk' frameborder='0' allowfullscreen >
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto pt-0 md:pt-20">
|
||||
<div class="flex flex-wrap bg-green-100">
|
||||
<div class="w-100 md:w-1/2 border border-green-500 text-gray-6 00 text-sm md:text-lg p-6">
|
||||
<div class="text-xl font-bold pb-4">No more joins joins, json, orms, just use GraphQL. Fetch all the data want in the structure you need.</div>
|
||||
<pre>
|
||||
query {
|
||||
thread {
|
||||
slug
|
||||
title
|
||||
published
|
||||
createdAt : created_at
|
||||
totalVotes : cached_votes_total
|
||||
totalPosts : cached_posts_total
|
||||
vote : thread_vote(where: { user_id: { eq: $user_id } }) {
|
||||
created_at
|
||||
}
|
||||
topics {
|
||||
slug
|
||||
name
|
||||
}
|
||||
author : me {
|
||||
slug
|
||||
}
|
||||
posts(first: 1, order_by: { score: desc }) {
|
||||
slug
|
||||
body
|
||||
published
|
||||
createdAt : created_at
|
||||
totalVotes : cached_votes_total
|
||||
totalComments : cached_comments_total
|
||||
vote {
|
||||
created_at
|
||||
}
|
||||
author : user {
|
||||
slug
|
||||
firstName : first_name
|
||||
lastName : last_name
|
||||
}
|
||||
}
|
||||
posts_cursor
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="w-100 md:w-1/2 border border-l md:border-l-0 border-green-500 text-blue-900 text-sm md:text-lg p-6">
|
||||
<div class="text-xl font-bold pb-4">Instant results using a single highly optimized SQL. It's just that simple.</div>
|
||||
<pre>
|
||||
{
|
||||
"data": {
|
||||
"thread": {
|
||||
"slug": "eveniet-ex-24",
|
||||
"vote": null,
|
||||
"posts": [
|
||||
{
|
||||
"body": "Dolor laborum harum sed sit est ducimus temporibus velit non nobis repudiandae nobis suscipit commodi voluptatem debitis sed voluptas sequi officia.",
|
||||
"slug": "illum-in-voluptas-1418",
|
||||
"vote": null,
|
||||
"author": {
|
||||
"slug": "sigurd-kemmer",
|
||||
"lastName": "Effertz",
|
||||
"firstName": "Brandt"
|
||||
},
|
||||
"createdAt": "2020-04-07T04:22:42.115874+00:00",
|
||||
"published": true,
|
||||
"totalVotes": 0,
|
||||
"totalComments": 2
|
||||
}
|
||||
],
|
||||
"title": "In aut qui deleniti quia dolore quasi porro tenetur voluptatem ut adita alias fugit explicabo.",
|
||||
"author": null,
|
||||
"topics": [
|
||||
{
|
||||
"name": "CloudRun",
|
||||
"slug": "cloud-run"
|
||||
},
|
||||
{
|
||||
"name": "Postgres",
|
||||
"slug": "postgres"
|
||||
}
|
||||
],
|
||||
"createdAt": "2020-04-07T04:22:38.099482+00:00",
|
||||
"published": true,
|
||||
"totalPosts": 24,
|
||||
"totalVotes": 0,
|
||||
"posts_cursor": "mpeBl6L+QfJHc3cmLkLDj9pOdEZYTt5KQtLsazG3TLITB3hJhg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-0 md:pt-20">
|
||||
<div class="container mx-auto px-10 md:px-0 py-32">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
Build Secure Apps
|
||||
</h1>
|
||||
<div class="flex flex-col text-2xl md:text-3xl">
|
||||
<card className="mb-1 p-8">
|
||||
<template #image><font-awesome-icon icon="portrait" class="text-red-500" /></template>
|
||||
<template #title>Role Based Access Control</template>
|
||||
<template #body>Dynamically assign roles like admin, manager or anon to specific users. Generate role specific queries at runtime. For example admins can get all users while others can only fetch their own user.</template>
|
||||
</card>
|
||||
<card className="mb-1 p-8">
|
||||
<template #image><font-awesome-icon icon="shield-alt" class="text-blue-500" /></template>
|
||||
<template #title>Prepared Statements</template>
|
||||
<template #body>An additional layer of protection from a variety of security issues like SQL injection. In production mode all queries are precompiled into prepared statements so only those can be executed. This also significantly speeds up all queries.</template>
|
||||
</card>
|
||||
<card className="p-8">
|
||||
<template #image><font-awesome-icon icon="lock" class="text-green-500"/></template>
|
||||
<template #title>Fuzz Tested Code</template>
|
||||
<template #body>Fuzzing is done by complex software that generates massives amounts of random input to detect if code is free of security bugs. Google uses fuzzing to protects everything from their cloud infrastructure to the Chrome browser.</template>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-0 md:py -20">
|
||||
<div class="container mx-auto">
|
||||
<h1 class="uppercase font-semibold text-xl text-blue-800 mb-4">
|
||||
More Features
|
||||
</h1>
|
||||
<div class="flex flex-col md:flex-row text-2xl md:text-3xl">
|
||||
<card className="mr-0 md:mr-1 mb-1 flex-col w-100 md:w-1/3 items-center">
|
||||
<!-- <template #image><img src="/arch-remote-join.svg" class="h-64"></template> -->
|
||||
<template #title>Remote Joins</template>
|
||||
<template #body>A powerful feature that allows you to query your database and remote REST APIs at the same time. For example fetch a user from the DB, his tweets from Twitter and his payments from Stripe with a single GraphQL query.</template>
|
||||
</card>
|
||||
<card className="mr-0 md:mr-1 mb-1 flex-col w-100 md:w-1/3">
|
||||
<!-- <template #image><img src="/arch-search.svg" class="h-64"></template> -->
|
||||
<template #title>Full Text Search</template>
|
||||
<template #body>Postgres has excellent full-text search built-in. You don't need another expensive service. Super Graph makes it super easy to use with keyword ranking and highlighting also supported.</template>
|
||||
</card>
|
||||
<card className="mb-1 flex-col w-100 md:w-1/3">
|
||||
<!-- <template #image><img src="/arch-bulk.svg" class="h-64"></template> -->
|
||||
<template #title>Bulk Inserts</template>
|
||||
<template #body>Efficiently insert, update and delete multiple items with a single query. Upserts are also supported</template>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mx-auto text-center py-8"
|
||||
v-if="data.footer"
|
||||
>
|
||||
{{ data.footer }}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavLink from '@theme/components/NavLink.vue'
|
||||
import Navbar from '@theme/components/Navbar.vue'
|
||||
import Card from './Card.vue'
|
||||
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faPortrait, faShieldAlt, faLock } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
library.add(faPortrait, faShieldAlt, faLock)
|
||||
|
||||
export default {
|
||||
components: { NavLink, Navbar, FontAwesomeIcon, Card },
|
||||
|
||||
computed: {
|
||||
data () {
|
||||
return this.$page.frontmatter
|
||||
},
|
||||
|
||||
actionLink () {
|
||||
return {
|
||||
link: this.data.actionLink,
|
||||
text: this.data.actionText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,6 +1,6 @@
|
||||
let ogprefix = 'og: http://ogp.me/ns#'
|
||||
let title = 'Super Graph'
|
||||
let description = 'An instant GraphQL API for your app. No code needed.'
|
||||
let description = 'Fetch data without code'
|
||||
let color = '#f42525'
|
||||
|
||||
module.exports = {
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
Before Width: | Height: | Size: 911 KiB After Width: | Height: | Size: 911 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
@ -1,6 +1,9 @@
|
||||
@tailwind base;
|
||||
|
||||
@css {
|
||||
body, .navbar, .navbar .links {
|
||||
@apply bg-white text-black border-0 !important;
|
||||
}
|
||||
h1 {
|
||||
@apply font-semibold text-3xl border-0 py-4
|
||||
}
|
@ -19,8 +19,8 @@ features:
|
||||
details: Compiles your GraphQL into a fast SQL query in realtime.
|
||||
- title: Built in GO
|
||||
details: Built in Go is a language created at Google to build fast and secure web services.
|
||||
- title: Ruby-on-Rails
|
||||
details: Can read Rails cookies and supports rails database conventions.
|
||||
- title: In your own Code
|
||||
details: Use as a library in your own GO code. Build faster save time and money.
|
||||
- title: Serverless
|
||||
details: Instant startup for scale to zero environments like Google Cloud Run, App Engine, AWS Lambda
|
||||
- title: Free and Open Source
|