diff --git a/.gitignore b/.gitignore index 967c560..5c88189 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ .vscode main .DS_Store -.swp \ No newline at end of file +.swp +main diff --git a/Dockerfile b/Dockerfile index 7110c71..7110835 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,9 @@ RUN apk update && \ apk add --no-cache upx=3.95-r2 RUN go get -u github.com/dosco/esc && \ - go get -u github.com/pilu/fresh + go get -u github.com/shanzi/wu && \ + go install github.com/shanzi/wu && \ + go get github.com/GeertJohan/go.rice/rice WORKDIR /app COPY . /app @@ -22,10 +24,13 @@ COPY --from=react-build /web/build/ ./web/build/ ENV GO111MODULE=on RUN go mod vendor +# RUN go generate ./... && \ +# CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o super-graph && \ +# upx --ultra-brute -qq super-graph && \ +# upx -t super-graph + RUN go generate ./... && \ - CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o super-graph && \ - upx --ultra-brute -qq super-graph && \ - upx -t super-graph + CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o super-graph # stage: 3 FROM alpine:latest diff --git a/config/prod.yml b/config/prod.yml index 7312bc1..908fc8f 100644 --- a/config/prod.yml +++ b/config/prod.yml @@ -80,7 +80,7 @@ database: type: postgres host: db port: 5432 - dbname: app_development + dbname: {{app_name_slug}}_development user: postgres password: '' #pool_size: 10 diff --git a/docker-compose.yml b/docker-compose.yml index a81ff1f..ea51297 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,16 +11,16 @@ services: # ports: # - "6379:6379" - rails_app: - build: rails-app/. - command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" - volumes: - - ./rails-app:/app - - /app/tmp - ports: - - "3000:3000" - depends_on: - - db + # rails_app: + # build: rails-app/. + # command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" + # volumes: + # - ./rails-app:/app + # - /app/tmp + # ports: + # - "3000:3000" + # depends_on: + # - db super_graph: build: @@ -34,9 +34,9 @@ services: volumes: - .:/app working_dir: /app - command: fresh -c fresh.conf + command: wu -pattern="*.go" go run main.go serv depends_on: - db - - rails_app + #- rails_app # - redis diff --git a/fresh.conf b/fresh.conf deleted file mode 100644 index 42d8d83..0000000 --- a/fresh.conf +++ /dev/null @@ -1,14 +0,0 @@ -root: . -tmp_path: ./tmp -build_name: runner-build -build_log: runner-build-errors.log -valid_ext: .go, .tpl, .tmpl, .html, .yml, *.list -no_rebuild_ext: .tpl, .tmpl, .html -ignored: web, tmp, vendor, rails-app, docs, slides, bench, corpus -build_delay: 600 -colors: 1 -log_color_main: cyan -log_color_build: yellow -log_color_runner: green -log_color_watcher: magenta -log_color_app: diff --git a/go.mod b/go.mod index f75225a..acf4225 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/dosco/super-graph require ( + github.com/GeertJohan/go.rice v1.0.0 github.com/Masterminds/semver v1.4.2 github.com/OneOfOne/xxhash v1.2.5 // indirect github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 @@ -35,6 +36,9 @@ require ( github.com/valyala/fasttemplate v1.0.1 github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 mellium.im/sasl v0.2.1 // indirect ) + +go 1.13 diff --git a/go.sum b/go.sum index 29abb78..c85ef76 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,9 @@ cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7h github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= @@ -15,6 +18,7 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 h1:+qz9Ga6l6lKw6fgvk5RMV5HQznSLvI8Zxajwdj4FhFg= github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3/go.mod h1:FlkD11RtgMTYjVuBnb7cxoHmQGqvPpCsr2atC88nl/M= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -51,6 +55,8 @@ github.com/cznic/ql v1.2.0/go.mod h1:FbpzhyZrqr0PVlK6ury+PoW3T0ODUV22OeWIxcaOrSE github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKXshKUbwUapqNncRrho4mkjQebgEHZLj8= +github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -171,6 +177,7 @@ github.com/jackc/puddle v1.0.0 h1:rbjAshlgKscNa7j0jAM0uNQflis5o2XUogPMVAwtcsM= github.com/jackc/puddle v1.0.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/tern v1.8.2 h1:+d9eK83fRS0dbf6nt+2tjILYF4FKG1O5xTFB8Lzc66U= github.com/jackc/tern v1.8.2/go.mod h1:AMppp2oyCT6rYnJHLLMmPWwahfFvdIVi6mr9gH81Nxs= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -209,6 +216,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= @@ -332,6 +340,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/hello/config/dev.yml b/hello/config/dev.yml new file mode 100644 index 0000000..c4541d7 --- /dev/null +++ b/hello/config/dev.yml @@ -0,0 +1,152 @@ +app_name: "Hello Development" +host_port: 0.0.0.0:8080 +web_ui: true + +# debug, info, warn, error, fatal, panic +log_level: "debug" + +# Disable this in development to get a list of +# queries used. When enabled super graph +# will only allow queries from this list +# List saved to ./config/allow.list +use_allow_list: false + +# Throw a 401 on auth failure for queries that need auth +# valid values: always, per_query, never +auth_fail_block: never + +# 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 + +# 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. Good for testing + header: X-User-ID + + 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: hello_database + user: postgres + password: '' + + #schema: "public" + #pool_size: 10 + #max_retries: 0 + #log_level: "debug" + + # Define variables here that you want to use in filters + # sub-queries must be wrapped in () + variables: + account_id: "(select account_id from users where id = $user_id)" + + # Define defaults to for the field key and values below + defaults: + # filter: ["{ user_id: { eq: $user_id } }"] + + # Field and table names that you wish to block + blocklist: + - ar_internal_metadata + - schema_migrations + - secret + - password + - encrypted + - token + + tables: + - name: users + # This filter will overwrite defaults.filter + # filter: ["{ id: { eq: $user_id } }"] + + # - name: products + # # Multiple filters are AND'd together + # filter: [ + # "{ price: { gt: 0 } }", + # "{ price: { lt: 8 } }" + # ] + + - name: customers + # No filter is used for this field not + # even defaults.filter + filter: none + + 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 + filter: ["{ id: { eq: $user_id } }"] + + # - name: posts + # filter: ["{ account_id: { _eq: $account_id } }"] \ No newline at end of file diff --git a/hello/config/migrations/100_init.sql b/hello/config/migrations/100_init.sql new file mode 100644 index 0000000..c06788d --- /dev/null +++ b/hello/config/migrations/100_init.sql @@ -0,0 +1,21 @@ +-- Write your migrate up statements here + +CREATE DATABASE hello_database + +-- CREATE TABLE public.users ( +-- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, +-- full_name text +-- email text UNIQUE NOT NULL CHECK (length(email) < 255), +-- encrypted_password text, +-- created_at timestamptz NOT NULL NOT NULL DEFAULT NOW(), +-- updated_at timestamptz NOT NULL NOT NULL DEFAULT NOW() +-- ); + +---- create above / drop below ---- + +-- Write your migrate down statements here. If this migration is irreversible +-- Then delete the separator line above. + +-- DROP TABLE public.users + +DROP DATABASE IF EXISTS hello_database diff --git a/hello/config/prod.yml b/hello/config/prod.yml new file mode 100644 index 0000000..2c39569 --- /dev/null +++ b/hello/config/prod.yml @@ -0,0 +1,144 @@ +app_name: "Hello Production" +host_port: 0.0.0.0:8080 +web_ui: false + +# debug, info, warn, error, fatal, panic, disable +log_level: "info" + +# Disable this in development to get a list of +# queries used. When enabled super graph +# will only allow queries from this list +# List saved to ./config/allow.list +use_allow_list: true + +# Throw a 401 on auth failure for queries that need auth +# valid values: always, per_query, never +auth_fail_block: always + +# 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 + +# 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. Good for testing + header: X-User-ID + + rails: + # Rails version this is used for reading the + # various cookies formats. + version: 5.2 + + # Found in 'Rails.application.config.secret_key_base' + secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566 + + # Remote cookie store. (memcache or redis) + # url: redis://127.0.0.1:6379 + # password: test + # max_idle: 80, + # max_active: 12000, + + # In most cases you don't need these + # salt: "encrypted cookie" + # sign_salt: "signed encrypted cookie" + # auth_salt: "authenticated encrypted cookie" + + # jwt: + # provider: auth0 + # secret: abc335bfcfdb04e50db5bb0a4d67ab9 + # public_key_file: /secrets/public_key.pem + # public_key_type: ecdsa #rsa + +database: + type: postgres + host: db + port: 5432 + dbname: hello_database + user: postgres + password: '' + #pool_size: 10 + #max_retries: 0 + #log_level: "debug" + + # Define variables here that you want to use in filters + # sub-queries must be wrapped in () + variables: + account_id: "(select account_id from users where id = $user_id)" + + # Define defaults to for the field key and values below + defaults: + filter: ["{ user_id: { eq: $user_id } }"] + + # Field and table names that you wish to block + blocklist: + - ar_internal_metadata + - schema_migrations + - secret + - password + - encrypted + - token + + tables: + - name: users + # This filter will overwrite defaults.filter + filter: ["{ id: { eq: $user_id } }"] + + - name: products + # Multiple filters are AND'd together + filter: [ + "{ price: { gt: 0 } }", + "{ price: { lt: 8 } }" + ] + + - name: customers + # No filter is used for this field not + # even defaults.filter + filter: none + + # remotes: + # - name: payments + # id: stripe_id + # url: http://rails_app:3000/stripe/$id + # path: data + # # pass_headers: + # # - cookie + # # - host + # set_headers: + # - name: Authorization + # value: Bearer + + - # You can create new fields that have a + # real db table backing them + name: me + table: users + filter: ["{ id: { eq: $user_id } }"] + + # - name: posts + # filter: ["{ account_id: { _eq: $account_id } }"] \ No newline at end of file diff --git a/hello/docker-compose.yml b/hello/docker-compose.yml new file mode 100644 index 0000000..7c53795 --- /dev/null +++ b/hello/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' +services: + db: + image: postgres + ports: + - "5432:5432" + + hello_api: + image: dosco/super-graph:latest + environment: + GO_ENV: "development" + volumes: + - ./config:/config + ports: + - "8080:8080" + depends_on: + - db \ No newline at end of file diff --git a/hello/seed.js b/hello/seed.js new file mode 100644 index 0000000..7c53795 --- /dev/null +++ b/hello/seed.js @@ -0,0 +1,17 @@ +version: '3' +services: + db: + image: postgres + ports: + - "5432:5432" + + hello_api: + image: dosco/super-graph:latest + environment: + GO_ENV: "development" + volumes: + - ./config:/config + ports: + - "8080:8080" + depends_on: + - db \ No newline at end of file diff --git a/psql/select.go b/psql/select.go index b0f0e91..f95571a 100644 --- a/psql/select.go +++ b/psql/select.go @@ -337,21 +337,21 @@ func (c *compilerContext) renderJoinClose(sel *qcode.Select) error { return nil } -func (c *compilerContext) renderJoinTable(sel *qcode.Select) { +func (c *compilerContext) renderJoinTable(sel *qcode.Select) error { parent := &c.s[sel.ParentID] rel, err := c.schema.GetRel(sel.Table, parent.Table) if err != nil { - panic(err) + return err } if rel.Type != RelOneToManyThrough { - return + return err } pt, err := c.schema.GetTable(parent.Table) if err != nil { - return + return err } //fmt.Fprintf(w, ` LEFT OUTER JOIN "%s" ON (("%s"."%s") = ("%s_%d"."%s"))`, @@ -363,6 +363,8 @@ func (c *compilerContext) renderJoinTable(sel *qcode.Select) { c.w.WriteString(`) = (`) colWithTableID(c.w, pt.Name, parent.ID, rel.Col1) c.w.WriteString(`))`) + + return nil } func (c *compilerContext) renderColumns(sel *qcode.Select) { @@ -537,10 +539,15 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, } if !isRoot { - c.renderJoinTable(sel) + if err := c.renderJoinTable(sel); err != nil { + return err + } c.w.WriteString(` WHERE (`) - c.renderRelationship(sel) + + if err := c.renderRelationship(sel); err != nil { + return err + } if isFil { c.w.WriteString(` AND `) @@ -610,12 +617,12 @@ func (c *compilerContext) renderOrderByColumns(sel *qcode.Select) { } } -func (c *compilerContext) renderRelationship(sel *qcode.Select) { +func (c *compilerContext) renderRelationship(sel *qcode.Select) error { parent := c.s[sel.ParentID] rel, err := c.schema.GetRel(sel.Table, parent.Table) if err != nil { - panic(err) + return err } switch rel.Type { @@ -646,6 +653,8 @@ func (c *compilerContext) renderRelationship(sel *qcode.Select) { colWithTable(c.w, rel.Through, rel.Col2) c.w.WriteString(`))`) } + + return nil } func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error { diff --git a/serv/allow.go b/serv/allow.go index 1d5c7b3..b209d79 100644 --- a/serv/allow.go +++ b/serv/allow.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "log" "os" + "path" "sort" "strings" ) @@ -31,20 +32,20 @@ type allowList struct { active bool } -func initAllowList(path string) { +func initAllowList(cpath string) { _allowList = allowList{ list: make(map[string]*allowItem), saveChan: make(chan *allowItem), active: true, } - if len(path) != 0 { - fp := fmt.Sprintf("%s/allow.list", path) + if len(cpath) != 0 { + fp := path.Join(cpath, "allow.list") if _, err := os.Stat(fp); err == nil { _allowList.filepath = fp } else if !os.IsNotExist(err) { - panic(err) + logger.Fatal().Err(err).Send() } } @@ -54,7 +55,7 @@ func initAllowList(path string) { if _, err := os.Stat(fp); err == nil { _allowList.filepath = fp } else if !os.IsNotExist(err) { - panic(err) + logger.Fatal().Err(err).Send() } } @@ -64,15 +65,25 @@ func initAllowList(path string) { if _, err := os.Stat(fp); err == nil { _allowList.filepath = fp } else if !os.IsNotExist(err) { - panic(err) + logger.Fatal().Err(err).Send() } } if len(_allowList.filepath) == 0 { - panic("allow.list not found") - } + if conf.UseAllowList { + logger.Fatal().Msg("allow.list not found") + } - _allowList.load() + if len(cpath) == 0 { + _allowList.filepath = "./config/allow.list" + } else { + _allowList.filepath = path.Join(cpath, "allow.list") + } + + logger.Warn().Msg("allow.list not found") + } else { + _allowList.load() + } go func() { for v := range _allowList.saveChan { @@ -182,7 +193,7 @@ func (al *allowList) save(item *allowItem) { f, err := os.Create(al.filepath) if err != nil { - logger.Warn().Err(err).Msg("Failed to write allow list to file") + logger.Warn().Err(err).Msgf("Failed to write allow list: %s", al.filepath) return } diff --git a/serv/auth_jwt.go b/serv/auth_jwt.go index 9ba4528..25ed785 100644 --- a/serv/auth_jwt.go +++ b/serv/auth_jwt.go @@ -35,7 +35,7 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc { case len(publicKeyFile) != 0: kd, err := ioutil.ReadFile(publicKeyFile) if err != nil { - panic(err) + logger.Fatal().Err(err).Send() } switch conf.Auth.JWT.PubKeyType { @@ -51,7 +51,7 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc { } if err != nil { - panic(err) + logger.Fatal().Err(err).Send() } } diff --git a/serv/auth_rails.go b/serv/auth_rails.go index 33cf3b8..c72c0d7 100644 --- a/serv/auth_rails.go +++ b/serv/auth_rails.go @@ -28,13 +28,13 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc { Dial: func() (redis.Conn, error) { c, err := redis.DialURL(conf.Auth.Rails.URL) if err != nil { - panic(err) + logger.Fatal().Err(err).Send() } pwd := conf.Auth.Rails.Password if len(pwd) != 0 { if _, err := c.Do("AUTH", pwd); err != nil { - panic(err) + logger.Fatal().Err(err).Send() } } return c, err diff --git a/serv/cmd.go b/serv/cmd.go index 8eff55b..a9c94b9 100644 --- a/serv/cmd.go +++ b/serv/cmd.go @@ -17,8 +17,6 @@ import ( "github.com/spf13/viper" ) -//go:generate esc -o static.go -ignore \\.DS_Store -prefix ../web/build -private -pkg serv ../web/build - const ( serverName = "Super Graph" @@ -42,11 +40,10 @@ var ( migrateCmd *cobra.Command statusCmd *cobra.Command newMigrationCmd *cobra.Command + initCmd *cobra.Command ) func Init() { - var err error - rootCmd = &cobra.Command{ Use: "super-graph", Short: "An instant high-performance GraphQL API. No code needed. https://supergraph.dev", @@ -110,6 +107,13 @@ e.g. tern migrate -d last Run: cmdNewMigration, } + initCmd = &cobra.Command{ + Use: "init APP-NAME", + Short: "Initialize a new application", + Long: "Generate all the required files to start on a new Super Graph app", + Run: cmdInit, + } + logger = initLog() rootCmd.Flags().StringVar(&confPath, @@ -118,16 +122,13 @@ e.g. tern migrate -d last //cmdMigrate.Flags().StringVarP(&cliOptions.destinationVersion, // "destination", "d", "last", "destination migration version") + rootCmd.AddCommand(initCmd) rootCmd.AddCommand(servCmd) rootCmd.AddCommand(seedCmd) rootCmd.AddCommand(migrateCmd) rootCmd.AddCommand(statusCmd) rootCmd.AddCommand(newMigrationCmd) - if conf, err = initConf(); err != nil { - logger.Fatal().Err(err).Msg("failed to read config") - } - if err := rootCmd.Execute(); err != nil { logger.Fatal().Err(err).Send() } diff --git a/serv/cmd_init.go b/serv/cmd_init.go new file mode 100644 index 0000000..9741513 --- /dev/null +++ b/serv/cmd_init.go @@ -0,0 +1,132 @@ +package serv + +import ( + "bytes" + "io/ioutil" + "os" + "path" + "strings" + "text/template" + + rice "github.com/GeertJohan/go.rice" + "github.com/spf13/cobra" +) + +func cmdInit(cmd *cobra.Command, args []string) { + if len(args) != 1 { + cmd.Help() + os.Exit(1) + } + + tmpl := newTempl(map[string]string{ + "app_name": strings.Title(strings.Join(args, " ")), + "app_name_slug": strings.ToLower(strings.Join(args, "_")), + }) + + // Create app folder and add relevant files + + name := args[0] + appPath := path.Join("./", name) + + ifNotExists(appPath, func(p string) error { + return os.Mkdir(p, os.ModePerm) + }) + + ifNotExists(path.Join(appPath, "seed.js"), func(p string) error { + if v, err := tmpl.get("docker-compose.yml"); err == nil { + return ioutil.WriteFile(p, v, 0644) + } else { + return err + } + }) + + ifNotExists(path.Join(appPath, "docker-compose.yml"), func(p string) error { + if v, err := tmpl.get("docker-compose.yml"); err == nil { + return ioutil.WriteFile(p, v, 0644) + } else { + return err + } + }) + + // Create app config folder and add relevant files + + appConfigPath := path.Join(appPath, "config") + + ifNotExists(appConfigPath, func(p string) error { + return os.Mkdir(p, os.ModePerm) + }) + + ifNotExists(path.Join(appConfigPath, "dev.yml"), func(p string) error { + if v, err := tmpl.get("dev.yml"); err == nil { + return ioutil.WriteFile(p, v, 0644) + } else { + return err + } + }) + + ifNotExists(path.Join(appConfigPath, "prod.yml"), func(p string) error { + if v, err := tmpl.get("prod.yml"); err == nil { + return ioutil.WriteFile(p, v, 0644) + } else { + return err + } + }) + + // Create app migrations folder and add relevant files + + appMigrationsPath := path.Join(appConfigPath, "migrations") + + ifNotExists(appMigrationsPath, func(p string) error { + return os.Mkdir(p, os.ModePerm) + }) + + ifNotExists(path.Join(appMigrationsPath, "100_init.sql"), func(p string) error { + if v, err := tmpl.get("100_init.sql"); err == nil { + return ioutil.WriteFile(p, v, 0644) + } else { + return err + } + }) + + logger.Info().Msgf("app '%s' initialized", name) +} + +type Templ struct { + *rice.Box + data map[string]string +} + +func newTempl(data map[string]string) *Templ { + return &Templ{rice.MustFindBox("../tmpl"), data} +} + +func (t *Templ) get(name string) ([]byte, error) { + v := t.MustString(name) + b := bytes.Buffer{} + tm := template.Must(template.New(name).Parse(v)) + + if err := tm.Execute(&b, t.data); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +func ifNotExists(filePath string, doFn func(string) error) { + _, err := os.Stat(filePath) + + if err == nil { + logger.Info().Err(err).Msgf("create skipped '%s' exists", filePath) + return + } + + if os.IsNotExist(err) == false { + logger.Fatal().Err(err).Msgf("unable to check if '%s' exists", filePath) + } + + err = doFn(filePath) + if err != nil { + logger.Fatal().Err(err).Msgf("unable to create '%s'", filePath) + } + logger.Info().Msgf("created '%s'", filePath) +} diff --git a/serv/cmd_seed.go b/serv/cmd_seed.go index 30b221b..bb39d37 100644 --- a/serv/cmd_seed.go +++ b/serv/cmd_seed.go @@ -16,6 +16,11 @@ import ( func cmdSeed(cmd *cobra.Command, args []string) { var err error + + if conf, err = initConf(); err != nil { + logger.Fatal().Err(err).Msg("failed to read config") + } + conf.UseAllowList = false db, err = initDBPool(conf) diff --git a/serv/cmd_serv.go b/serv/cmd_serv.go index 16ee1c5..c5718db 100644 --- a/serv/cmd_serv.go +++ b/serv/cmd_serv.go @@ -7,6 +7,10 @@ import ( func cmdServ(cmd *cobra.Command, args []string) { var err error + if conf, err = initConf(); err != nil { + logger.Fatal().Err(err).Msg("failed to read config") + } + db, err = initDBPool(conf) if err != nil { logger.Fatal().Err(err).Msg("failed to connect to database") diff --git a/serv/cmd_tern.go b/serv/cmd_tern.go index 3481f77..2b17499 100644 --- a/serv/cmd_tern.go +++ b/serv/cmd_tern.go @@ -40,6 +40,12 @@ func cmdNewMigration(cmd *cobra.Command, args []string) { os.Exit(1) } + var err error + + if conf, err = initConf(); err != nil { + logger.Fatal().Err(err).Msg("failed to read config") + } + name := args[0] m, err := migrate.FindMigrations(conf.MigrationsPath) @@ -64,10 +70,16 @@ func cmdNewMigration(cmd *cobra.Command, args []string) { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - logger.Info().Msgf("created migration '%s'\n", mpath) + logger.Info().Msgf("created migration '%s'", mpath) } func cmdMigrate(cmd *cobra.Command, args []string) { + var err error + + if conf, err = initConf(); err != nil { + logger.Fatal().Err(err).Msg("failed to read config") + } + conn, err := initDB(conf) if err != nil { logger.Fatal().Err(err).Msg("failed to connect to database") @@ -157,6 +169,12 @@ func cmdMigrate(cmd *cobra.Command, args []string) { } func cmdStatus(cmd *cobra.Command, args []string) { + var err error + + if conf, err = initConf(); err != nil { + logger.Fatal().Err(err).Msg("failed to read config") + } + conn, err := initDB(conf) if err != nil { logger.Fatal().Err(err).Msg("failed to connect to database") diff --git a/serv/prepare.go b/serv/prepare.go index bee390c..40b024c 100644 --- a/serv/prepare.go +++ b/serv/prepare.go @@ -30,7 +30,7 @@ func initPreparedList() { for k, v := range _allowList.list { err := prepareStmt(k, v.gql, v.vars) if err != nil { - panic(err) + logger.Fatal().Err(err).Send() } } } diff --git a/serv/reload.go b/serv/reload.go index bf90fc5..bf434b3 100644 --- a/serv/reload.go +++ b/serv/reload.go @@ -161,7 +161,7 @@ func Do(log func(string, ...interface{}), additional ...dir) error { func ReExec() { err := syscall.Exec(binSelf, append([]string{binSelf}, os.Args[1:]...), os.Environ()) if err != nil { - panic(fmt.Sprintf("cannot restart: %v", err)) + logger.Fatal().Err(err).Msg("cannot restart") } } diff --git a/serv/serv.go b/serv/serv.go index f3c0df9..15a3e49 100644 --- a/serv/serv.go +++ b/serv/serv.go @@ -9,6 +9,7 @@ import ( "strings" "time" + rice "github.com/GeertJohan/go.rice" "github.com/dosco/super-graph/psql" "github.com/dosco/super-graph/qcode" ) @@ -38,22 +39,22 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) { return qc, pc, nil } -func initWatcher(path string) { +func initWatcher(cpath string) { if conf.WatchAndReload == false { return } var d dir - if len(path) == 0 || path == "./" { + if len(cpath) == 0 || cpath == "./" { d = Dir("./config", ReExec) } else { - d = Dir(path, ReExec) + d = Dir(cpath, ReExec) } go func() { err := Do(logger.Printf, d) if err != nil { - panic(err) + logger.Fatal().Err(err).Send() } }() } @@ -109,7 +110,7 @@ func routeHandler() http.Handler { mux.Handle("/api/v1/graphql", withAuth(apiv1Http)) if conf.WebUI { - mux.Handle("/", http.FileServer(_escFS(false))) + mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox())) } fn := func(w http.ResponseWriter, r *http.Request) { diff --git a/test.yml b/test.yml new file mode 100644 index 0000000..d073f15 --- /dev/null +++ b/test.yml @@ -0,0 +1,57 @@ +app_name: Test App +auth: + cookie: _app_session + header: X-User-ID + rails: + max_active: 12000 + max_idle: 80 + secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566 + version: 5.2 + type: rails +auth_fail_block: never +database: + dbname: app_development + defaults: + blocklist: + - ar_internal_metadata + - schema_migrations + - secret + - password + - encrypted + - token + host: db + password: "" + port: 5432 + schema: public + tables: + - name: users + - filter: none + name: customers + remotes: + - id: stripe_id + name: payments + pass_headers: + - cookie + path: data + set_headers: + - name: Host + value: 0.0.0.0 + url: http://rails_app:3000/stripe/$id + - filter: + - '{ id: { eq: $user_id } }' + name: me + table: users + type: postgres + user: postgres + variables: + account_id: (select account_id from users where id = $user_id) +enable_tracing: true +env: development +host_port: 0.0.0.0:8080 +log_level: debug +migrations_path: ./config/migrations +port: "8080" +reload_on_config_change: true +seed_file: seed.js +use_allow_list: false +web_ui: true diff --git a/tmpl/100_init.sql b/tmpl/100_init.sql new file mode 100644 index 0000000..be5deb1 --- /dev/null +++ b/tmpl/100_init.sql @@ -0,0 +1,21 @@ +-- Write your migrate up statements here + +CREATE DATABASE {{ .app_name_slug }}_database + +-- CREATE TABLE public.users ( +-- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, +-- full_name text +-- email text UNIQUE NOT NULL CHECK (length(email) < 255), +-- encrypted_password text, +-- created_at timestamptz NOT NULL NOT NULL DEFAULT NOW(), +-- updated_at timestamptz NOT NULL NOT NULL DEFAULT NOW() +-- ); + +---- create above / drop below ---- + +-- Write your migrate down statements here. If this migration is irreversible +-- Then delete the separator line above. + +-- DROP TABLE public.users + +DROP DATABASE IF EXISTS {{ .app_name_slug }}_database diff --git a/tmpl/dev.yml b/tmpl/dev.yml new file mode 100644 index 0000000..346e84f --- /dev/null +++ b/tmpl/dev.yml @@ -0,0 +1,152 @@ +app_name: "{{ .app_name }} Development" +host_port: 0.0.0.0:8080 +web_ui: true + +# debug, info, warn, error, fatal, panic +log_level: "debug" + +# Disable this in development to get a list of +# queries used. When enabled super graph +# will only allow queries from this list +# List saved to ./config/allow.list +use_allow_list: false + +# Throw a 401 on auth failure for queries that need auth +# valid values: always, per_query, never +auth_fail_block: never + +# 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 + +# 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. Good for testing + header: X-User-ID + + 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_name_slug }}_database + user: postgres + password: '' + + #schema: "public" + #pool_size: 10 + #max_retries: 0 + #log_level: "debug" + + # Define variables here that you want to use in filters + # sub-queries must be wrapped in () + variables: + account_id: "(select account_id from users where id = $user_id)" + + # Define defaults to for the field key and values below + defaults: + # filter: ["{ user_id: { eq: $user_id } }"] + + # Field and table names that you wish to block + blocklist: + - ar_internal_metadata + - schema_migrations + - secret + - password + - encrypted + - token + + tables: + - name: users + # This filter will overwrite defaults.filter + # filter: ["{ id: { eq: $user_id } }"] + + # - name: products + # # Multiple filters are AND'd together + # filter: [ + # "{ price: { gt: 0 } }", + # "{ price: { lt: 8 } }" + # ] + + - name: customers + # No filter is used for this field not + # even defaults.filter + filter: none + + 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 + filter: ["{ id: { eq: $user_id } }"] + + # - name: posts + # filter: ["{ account_id: { _eq: $account_id } }"] \ No newline at end of file diff --git a/tmpl/docker-compose.yml b/tmpl/docker-compose.yml new file mode 100644 index 0000000..d89773c --- /dev/null +++ b/tmpl/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' +services: + db: + image: postgres + ports: + - "5432:5432" + + {{ .app_name_slug }}_api: + image: dosco/super-graph:latest + environment: + GO_ENV: "development" + volumes: + - ./config:/config + ports: + - "8080:8080" + depends_on: + - db \ No newline at end of file diff --git a/tmpl/prod.yml b/tmpl/prod.yml new file mode 100644 index 0000000..dc36a87 --- /dev/null +++ b/tmpl/prod.yml @@ -0,0 +1,144 @@ +app_name: "{{ .app_name }} Production" +host_port: 0.0.0.0:8080 +web_ui: false + +# debug, info, warn, error, fatal, panic, disable +log_level: "info" + +# Disable this in development to get a list of +# queries used. When enabled super graph +# will only allow queries from this list +# List saved to ./config/allow.list +use_allow_list: true + +# Throw a 401 on auth failure for queries that need auth +# valid values: always, per_query, never +auth_fail_block: always + +# 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 + +# 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. Good for testing + header: X-User-ID + + rails: + # Rails version this is used for reading the + # various cookies formats. + version: 5.2 + + # Found in 'Rails.application.config.secret_key_base' + secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566 + + # Remote cookie store. (memcache or redis) + # url: redis://127.0.0.1:6379 + # password: test + # max_idle: 80, + # max_active: 12000, + + # In most cases you don't need these + # salt: "encrypted cookie" + # sign_salt: "signed encrypted cookie" + # auth_salt: "authenticated encrypted cookie" + + # jwt: + # provider: auth0 + # secret: abc335bfcfdb04e50db5bb0a4d67ab9 + # public_key_file: /secrets/public_key.pem + # public_key_type: ecdsa #rsa + +database: + type: postgres + host: db + port: 5432 + dbname: {{ .app_name_slug }}_database + user: postgres + password: '' + #pool_size: 10 + #max_retries: 0 + #log_level: "debug" + + # Define variables here that you want to use in filters + # sub-queries must be wrapped in () + variables: + account_id: "(select account_id from users where id = $user_id)" + + # Define defaults to for the field key and values below + defaults: + filter: ["{ user_id: { eq: $user_id } }"] + + # Field and table names that you wish to block + blocklist: + - ar_internal_metadata + - schema_migrations + - secret + - password + - encrypted + - token + + tables: + - name: users + # This filter will overwrite defaults.filter + filter: ["{ id: { eq: $user_id } }"] + + - name: products + # Multiple filters are AND'd together + filter: [ + "{ price: { gt: 0 } }", + "{ price: { lt: 8 } }" + ] + + - name: customers + # No filter is used for this field not + # even defaults.filter + filter: none + + # remotes: + # - name: payments + # id: stripe_id + # url: http://rails_app:3000/stripe/$id + # path: data + # # pass_headers: + # # - cookie + # # - host + # set_headers: + # - name: Authorization + # value: Bearer + + - # You can create new fields that have a + # real db table backing them + name: me + table: users + filter: ["{ id: { eq: $user_id } }"] + + # - name: posts + # filter: ["{ account_id: { _eq: $account_id } }"] \ No newline at end of file diff --git a/tmpl/seed.js b/tmpl/seed.js new file mode 100644 index 0000000..33f7b9a --- /dev/null +++ b/tmpl/seed.js @@ -0,0 +1,23 @@ +// Example script to seed database + +var users = []; + +for (i = 0; i < 10; i++) { + var pwd = fake.password() + var data = { + first_name: fake.first_name(), + last_name: fake.last_name(), + email: fake.email(), + password: pwd, + password_confirmation: pwd + } + + var res = graphql(" \ + mutation { \ + user(insert: $data) { \ + id \ + } \ + }", { data: data }) + + users.push(res.user) +} \ No newline at end of file