diff --git a/.gitignore b/.gitignore index 4b87d43..d4158dc 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ .release main super-graph -supergraph *-fuzz.zip crashers suppressions diff --git a/Dockerfile b/Dockerfile index e95e163..42c38d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ COPY /cmd/internal/serv/web/ ./ RUN yarn RUN yarn build + + # stage: 2 FROM golang:1.14-alpine as go-build RUN apk update && \ @@ -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 / diff --git a/README.md b/README.md index 36a37b4..247e73b 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,74 @@ - - - + ### Build web products faster. Secure high performance GraphQL -![Apache Public License 2.0](https://img.shields.io/github/license/dosco/super-graph.svg) -![Docker build](https://img.shields.io/docker/cloud/build/dosco/super-graph.svg) -![Cloud native](https://img.shields.io/badge/cloud--native-enabled-blue.svg) +[![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4.svg)](https://pkg.go.dev/github.com/dosco/super-graph/core?tab=doc) +![Apache 2.0](https://img.shields.io/github/license/dosco/super-graph.svg?style=flat-square) +![Docker build](https://img.shields.io/docker/cloud/build/dosco/super-graph.svg?style=flat-square) +![Cloud native](https://img.shields.io/badge/cloud--native-enabled-blue.svg?style=flat-squareg) [![Discord Chat](https://img.shields.io/discord/628796009539043348.svg)](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 +git clone https://github.com/dosco/super-graph +cd ./super-graph +make install -![GraphQL](docs/.vuepress/public/graphql.png?raw=true "") +super-graph new +``` +## Using it in your own code -## The story of Super Graph? +```golang +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)) +} +``` + +## 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 +85,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 +99,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 -``` ## Documentation diff --git a/config/allow.list b/config/allow.list new file mode 100644 index 0000000..71c4cea --- /dev/null +++ b/config/allow.list @@ -0,0 +1,755 @@ +# http://localhost:8080/ + +variables { + "data": [ + { + "name": "Protect Ya Neck", + "created_at": "now", + "updated_at": "now" + }, + { + "name": "Enter the Wu-Tang", + "created_at": "now", + "updated_at": "now" + } + ] +} + +mutation { + products(insert: $data) { + id + name + } +} + +variables { + "update": { + "name": "Wu-Tang", + "description": "No description needed" + }, + "product_id": 1 +} + +mutation { + products(id: $product_id, update: $update) { + id + name + description + } +} + +query { + users { + id + email + picture: avatar + products(limit: 2, where: {price: {gt: 10}}) { + id + name + description + } + } +} + +variables { + "data": [ + { + "name": "Gumbo1", + "created_at": "now", + "updated_at": "now" + }, + { + "name": "Gumbo2", + "created_at": "now", + "updated_at": "now" + } + ] +} + +mutation { + products(id: 199, delete: true) { + id + name + } +} + +query { + products { + id + name + user { + email + } + } +} + +variables { + "data": { + "product_id": 5 + } +} + +mutation { + products(id: $product_id, delete: true) { + id + name + } +} + +query { + products { + id + name + price + users { + email + } + } +} + +variables { + "data": { + "email": "gfk@myspace.com", + "full_name": "Ghostface Killah", + "created_at": "now", + "updated_at": "now" + } +} + +mutation { + user(insert: $data) { + id + } +} + +variables { + "update": { + "name": "Helloo", + "description": "World \u003c\u003e" + }, + "user": 123 +} + +mutation { + products(id: 5, update: $update) { + id + name + description + } +} + +variables { + "data": { + "name": "WOOO", + "price": 50.5 + } +} + +mutation { + products(insert: $data) { + id + name + } +} + +query getProducts { + products { + id + name + price + description + } +} + +query { + deals { + id + name + price + } +} + +variables { + "beer": "smoke" +} + +query beerSearch { + products(search: $beer) { + id + name + search_rank + search_headline_description + } +} + +query { + user { + id + full_name + } +} + +variables { + "data": { + "email": "goo1@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now" + } + } +} + +mutation { + user(insert: $data) { + id + full_name + email + product { + id + name + price + } + } +} + +variables { + "data": { + "email": "goo12@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": [ + { + "name": "Banana 1", + "price": 1.1, + "created_at": "now", + "updated_at": "now" + }, + { + "name": "Banana 2", + "price": 2.2, + "created_at": "now", + "updated_at": "now" + } + ] + } +} + +mutation { + user(insert: $data) { + id + full_name + email + products { + id + name + price + } + } +} + +variables { + "data": { + "name": "Banana 3", + "price": 1.1, + "created_at": "now", + "updated_at": "now", + "user": { + "email": "a2@a.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now" + } + } +} + +mutation { + products(insert: $data) { + id + name + price + user { + id + full_name + email + } + } +} + +variables { + "update": { + "name": "my_name", + "description": "my_desc" + } +} + +mutation { + product(id: 15, update: $update, where: {id: {eq: 1}}) { + id + name + } +} + +variables { + "update": { + "name": "my_name", + "description": "my_desc" + } +} + +mutation { + product(update: $update, where: {id: {eq: 1}}) { + id + name + } +} + +variables { + "update": { + "name": "my_name 2", + "description": "my_desc 2" + } +} + +mutation { + product(update: $update, where: {id: {eq: 1}}) { + id + name + description + } +} + +variables { + "data": { + "sale_type": "tuutuu", + "quantity": 5, + "due_date": "now", + "customer": { + "email": "thedude1@rug.com", + "full_name": "The Dude" + }, + "product": { + "name": "Apple", + "price": 1.25 + } + } +} + +mutation { + purchase(update: $data, id: 5) { + sale_type + quantity + due_date + customer { + id + full_name + email + } + product { + id + name + price + } + } +} + +variables { + "data": { + "email": "thedude@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "where": { + "id": 2 + }, + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now" + } + } +} + +mutation { + user(update: $data, where: {id: {eq: 8}}) { + id + full_name + email + product { + id + name + price + } + } +} + +variables { + "data": { + "email": "thedude@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "where": { + "id": 2 + }, + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now" + } + } +} + +query { + user(where: {id: {eq: 8}}) { + id + product { + id + name + price + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "email": "thedude@rug.com" + } + } +} + +query { + user { + email + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "email": "booboo@demo.com" + } + } +} + +mutation { + product(update: $data, id: 6) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "email": "booboo@demo.com" + } + } +} + +query { + product(id: 6) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "email": "thedude123@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "connect": { + "id": 7 + }, + "disconnect": { + "id": 8 + } + } + } +} + +mutation { + user(update: $data, id: 6) { + id + full_name + email + product { + id + name + price + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { + "id": 5, + "email": "test@test.com" + } + } + } +} + +mutation { + product(update: $data, id: 9) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "email": "thed44ude@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "connect": { + "id": 5 + } + } + } +} + +mutation { + user(insert: $data) { + id + full_name + email + product { + id + name + price + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { + "id": 5 + } + } + } +} + +mutation { + product(insert: $data) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": [ + { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { + "id": 6 + } + } + }, + { + "name": "Coconut", + "price": 2.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { + "id": 3 + } + } + } + ] +} + +mutation { + products(insert: $data) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": [ + { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now" + }, + { + "name": "Coconut", + "price": 2.25, + "created_at": "now", + "updated_at": "now" + } + ] +} + +mutation { + products(insert: $data) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "connect": { + "id": 5, + "email": "test@test.com" + } + } + } +} + +mutation { + product(update: $data, id: 9) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "connect": { + "id": 5 + } + } + } +} + +mutation { + product(update: $data, id: 9) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "disconnect": { + "id": 5 + } + } + } +} + +mutation { + product(update: $data, id: 9) { + id + name + user_id + } +} + + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "disconnect": { + "id": 5 + } + } + } +} + +mutation { + product(update: $data, id: 2) { + id + name + user_id + } +} + + diff --git a/config/dev.yml b/config/dev.yml new file mode 100644 index 0000000..d5ef922 --- /dev/null +++ b/config/dev.yml @@ -0,0 +1,226 @@ +app_name: "Super Graph Development" +host_port: 0.0.0.0:8080 +web_ui: true + +# debug, error, warn, info, none +log_level: "debug" + +# enable or disable http compression (uses gzip) +http_compress: true + +# When production mode is 'true' only queries +# from the allow list are permitted. +# When it's 'false' all queries are saved to the +# the allow list in ./config/allow.list +production: false + +# Throw a 401 on auth failure for queries that need auth +auth_fail_block: false + +# Latency tracing for database queries and remote joins +# the resulting latency information is returned with the +# response +enable_tracing: true + +# Watch the config folder and reload Super Graph +# with the new configs when a change is detected +reload_on_config_change: true + +# File that points to the database seeding script +# seed_file: seed.js + +# Path pointing to where the migrations can be found +migrations_path: ./config/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 +# SG_DATABASE_USER +# SG_DATABASE_PASSWORD + +# Auth related environment Variables +# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE +# SG_AUTH_RAILS_REDIS_URL +# SG_AUTH_RAILS_REDIS_PASSWORD +# SG_AUTH_JWT_PUBLIC_KEY_FILE + +# inflections: +# person: people +# sheep: sheep + +auth: + # Can be 'rails' or 'jwt' + type: rails + cookie: _app_session + + # Comment this out if you want to disable setting + # the user_id via a header for testing. + # Disable in production + creds_in_header: true + + rails: + # Rails version this is used for reading the + # various cookies formats. + version: 5.2 + + # Found in 'Rails.application.config.secret_key_base' + secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566 + + # Remote cookie store. (memcache or redis) + # url: redis://redis:6379 + # password: "" + # max_idle: 80 + # max_active: 12000 + + # In most cases you don't need these + # salt: "encrypted cookie" + # sign_salt: "signed encrypted cookie" + # auth_salt: "authenticated encrypted cookie" + + # jwt: + # provider: auth0 + # secret: abc335bfcfdb04e50db5bb0a4d67ab9 + # public_key_file: /secrets/public_key.pem + # public_key_type: ecdsa #rsa + +database: + type: postgres + host: db + port: 5432 + dbname: app_development + user: postgres + password: postgres + + #schema: "public" + #pool_size: 10 + #max_retries: 0 + #log_level: "debug" + + # Set session variable "user.id" to the user id + # Enable this if you need the user id in triggers, etc + set_user_id: false + + # 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" + + # Field and table names that you wish to block + blocklist: + - ar_internal_metadata + - schema_migrations + - secret + - password + - encrypted + - token + +tables: + - name: customers + remotes: + - name: payments + id: stripe_id + url: http://rails_app:3000/stripe/$id + path: data + # debug: true + pass_headers: + - cookie + set_headers: + - name: Host + value: 0.0.0.0 + # - name: Authorization + # value: Bearer + + - # You can create new fields that have a + # real db table backing them + name: me + table: users + + - name: deals + table: products + + - name: users + columns: + - name: email + related_to: products.name + + +roles_query: "SELECT * FROM users WHERE id = $user_id" + +roles: + - name: anon + tables: + - name: products + query: + limit: 10 + columns: ["id", "name", "description" ] + aggregation: false + + insert: + block: false + + update: + block: false + + delete: + block: false + + - name: deals + query: + limit: 3 + aggregation: false + + - name: purchases + query: + limit: 3 + aggregation: false + + - name: user + tables: + - name: users + query: + filters: ["{ id: { _eq: $user_id } }"] + + - name: products + query: + limit: 50 + filters: ["{ user_id: { eq: $user_id } }"] + disable_functions: false + + insert: + filters: ["{ user_id: { eq: $user_id } }"] + presets: + - user_id: "$user_id" + - created_at: "now" + - updated_at: "now" + + update: + filters: ["{ user_id: { eq: $user_id } }"] + columns: + - id + - name + presets: + - updated_at: "now" + + delete: + block: true + + - name: admin + match: id = 1000 + tables: + - name: users + filters: [] diff --git a/config/prod.yml b/config/prod.yml new file mode 100644 index 0000000..ac54027 --- /dev/null +++ b/config/prod.yml @@ -0,0 +1,67 @@ +# Inherit config from this other config file +# so I only need to overwrite some values +inherits: dev + +app_name: "Super Graph Production" +host_port: 0.0.0.0:8080 +web_ui: false + +# debug, error, warn, info, none +log_level: "info" + +# enable or disable http compression (uses gzip) +http_compress: true + +# When production mode is 'true' only queries +# from the allow list are permitted. +# When it's 'false' all queries are saved to the +# the allow list in ./config/allow.list +production: true + +# Throw a 401 on auth failure for queries that need auth +auth_fail_block: true + +# Latency tracing for database queries and remote joins +# the resulting latency information is returned with the +# response +enable_tracing: true + +# File that points to the database seeding script +# seed_file: seed.js + +# Path pointing to where the migrations can be found +# migrations_path: migrations + +# Secret key for general encryption operations like +# encrypting the cursor data +# secret_key: supercalifajalistics + +# Postgres related environment Variables +# SG_DATABASE_HOST +# SG_DATABASE_PORT +# SG_DATABASE_USER +# SG_DATABASE_PASSWORD + +# Auth related environment Variables +# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE +# SG_AUTH_RAILS_REDIS_URL +# SG_AUTH_RAILS_REDIS_PASSWORD +# SG_AUTH_JWT_PUBLIC_KEY_FILE + +database: + type: postgres + host: db + port: 5432 + dbname: app_production + user: postgres + password: postgres + #pool_size: 10 + #max_retries: 0 + #log_level: "debug" + + # Set session variable "user.id" to the user id + # Enable this if you need the user id in triggers, etc + set_user_id: false + + # database ping timeout is used for db health checking + ping_timeout: 5m \ No newline at end of file diff --git a/config/seed.js b/config/seed.js new file mode 100644 index 0000000..bacc25f --- /dev/null +++ b/config/seed.js @@ -0,0 +1,116 @@ +var user_count = 10 + customer_count = 100 + product_count = 50 + purchase_count = 100 + +var users = [] + customers = [] + products = [] + +for (i = 0; i < user_count; i++) { + var pwd = fake.password() + var data = { + full_name: fake.name(), + avatar: fake.avatar_url(200), + phone: fake.phone(), + email: fake.email(), + password: pwd, + password_confirmation: pwd, + created_at: "now", + updated_at: "now" + } + + var res = graphql(" \ + mutation { \ + user(insert: $data) { \ + id \ + } \ + }", { data: data }) + + users.push(res.user) +} + +for (i = 0; i < product_count; i++) { + var n = Math.floor(Math.random() * users.length) + var user = users[n] + + var desc = [ + fake.beer_style(), + fake.beer_hop(), + fake.beer_yeast(), + fake.beer_ibu(), + fake.beer_alcohol(), + fake.beer_blg(), + ].join(", ") + + var data = { + name: fake.beer_name(), + description: desc, + price: fake.price() + //user_id: user.id, + //created_at: "now", + //updated_at: "now" + } + + var res = graphql(" \ + mutation { \ + product(insert: $data) { \ + id \ + } \ + }", { data: data }, { + user_id: 5 + }) + products.push(res.product) +} + +for (i = 0; i < customer_count; i++) { + var pwd = fake.password() + + var data = { + stripe_id: "CUS-" + fake.uuid(), + full_name: fake.name(), + phone: fake.phone(), + email: fake.email(), + password: pwd, + password_confirmation: pwd, + created_at: "now", + updated_at: "now" + } + + var res = graphql(" \ + mutation { \ + customer(insert: $data) { \ + id \ + } \ + }", { data: data }) + customers.push(res.customer) +} + +for (i = 0; i < purchase_count; i++) { + var sale_type = fake.rand_string(["rented", "bought"]) + + if (sale_type === "rented") { + var due_date = fake.date() + var returned = fake.date() + } + + var data = { + customer_id: customers[Math.floor(Math.random() * customer_count)].id, + product_id: products[Math.floor(Math.random() * product_count)].id, + sale_type: sale_type, + quantity: Math.floor(Math.random() * 10), + due_date: due_date, + returned: returned, + created_at: "now", + updated_at: "now" + } + + var res = graphql(" \ + mutation { \ + purchase(insert: $data) { \ + id \ + } \ + }", { data: data }) + + console.log(res) +} \ No newline at end of file diff --git a/docs/guide/.vuepress/components/Card.vue b/docs/guide/.vuepress/components/Card.vue index 9902d00..7232033 100644 --- a/docs/guide/.vuepress/components/Card.vue +++ b/docs/guide/.vuepress/components/Card.vue @@ -1,5 +1,5 @@