├── zero-rails
├── log
│ └── .keep
├── tmp
│ └── .keep
├── lib
│ ├── assets
│ │ └── .keep
│ └── tasks
│ │ └── .keep
├── test
│ ├── models
│ │ └── .keep
│ ├── controllers
│ │ └── .keep
│ ├── fixtures
│ │ ├── .keep
│ │ └── files
│ │ │ └── .keep
│ ├── helpers
│ │ └── .keep
│ ├── integration
│ │ └── .keep
│ ├── mailers
│ │ └── .keep
│ └── test_helper.rb
├── public
│ ├── favicon.ico
│ ├── apple-touch-icon.png
│ ├── apple-touch-icon-precomposed.png
│ ├── robots.txt
│ ├── 500.html
│ ├── 422.html
│ └── 404.html
├── app
│ ├── assets
│ │ ├── images
│ │ │ └── .keep
│ │ ├── javascripts
│ │ │ ├── channels
│ │ │ │ └── .keep
│ │ │ ├── cable.coffee
│ │ │ └── application.js
│ │ ├── config
│ │ │ └── manifest.js
│ │ └── stylesheets
│ │ │ └── application.css
│ ├── models
│ │ ├── concerns
│ │ │ └── .keep
│ │ ├── application_record.rb
│ │ ├── person.rb
│ │ └── schema.rb
│ ├── controllers
│ │ ├── concerns
│ │ │ └── .keep
│ │ ├── people_controller.rb
│ │ ├── graphql_controller.rb
│ │ └── application_controller.rb
│ ├── views
│ │ └── layouts
│ │ │ ├── mailer.text.erb
│ │ │ ├── mailer.html.erb
│ │ │ └── application.html.erb
│ ├── helpers
│ │ ├── people_helper.rb
│ │ └── application_helper.rb
│ ├── jobs
│ │ └── application_job.rb
│ ├── mailers
│ │ └── application_mailer.rb
│ ├── channels
│ │ └── application_cable
│ │ │ ├── channel.rb
│ │ │ └── connection.rb
│ └── serializers
│ │ └── person_serializer.rb
├── vendor
│ └── assets
│ │ ├── javascripts
│ │ └── .keep
│ │ └── stylesheets
│ │ └── .keep
├── bin
│ ├── bundle
│ ├── rake
│ ├── rails
│ ├── spring
│ ├── update
│ └── setup
├── config
│ ├── boot.rb
│ ├── environment.rb
│ ├── initializers
│ │ ├── session_store.rb
│ │ ├── mime_types.rb
│ │ ├── per_form_csrf_tokens.rb
│ │ ├── application_controller_renderer.rb
│ │ ├── request_forgery_protection.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── cookies_serializer.rb
│ │ ├── callback_terminator.rb
│ │ ├── active_record_belongs_to_required_by_default.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── assets.rb
│ │ ├── wrap_parameters.rb
│ │ └── inflections.rb
│ ├── routes.rb
│ ├── cable.yml
│ ├── application.rb
│ ├── database.yml
│ ├── locales
│ │ └── en.yml
│ ├── secrets.yml
│ ├── environments
│ │ ├── test.rb
│ │ ├── development.rb
│ │ └── production.rb
│ └── puma.rb
├── Rakefile
├── config.ru
├── db
│ ├── migrate
│ │ ├── 20160413032518_create_people.rb
│ │ └── 20160413033451_create_friendships.rb
│ ├── seeds.rb
│ └── schema.rb
├── README.md
├── .gitignore
├── Gemfile
└── Gemfile.lock
├── zero-django
├── people
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0002_person_friends.py
│ │ └── 0001_initial.py
│ ├── admin.py
│ ├── tests.py
│ ├── apps.py
│ ├── urls.py
│ ├── views.py
│ └── models.py
├── zero_django
│ ├── models.py
│ ├── __init__.py
│ ├── urls.py
│ ├── wsgi.py
│ ├── schema.py
│ └── settings.py
├── .gitignore
├── .python-version
├── requirements.txt
├── db.sqlite3
├── manage.py
└── README.md
├── zero-node
├── .gitignore
├── README.md
├── package.json
├── index.js
└── schema.js
├── zero-scala
├── project
│ ├── build.properties
│ └── plugins.sbt
├── .gitignore
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── application.conf
│ │ └── scala
│ │ │ └── SchemaSpec.scala
│ └── main
│ │ ├── resources
│ │ ├── application.conf
│ │ └── graphiql.html
│ │ └── scala
│ │ ├── SchemaDefinition.scala
│ │ ├── Server.scala
│ │ └── Repository.scala
├── build.sbt
└── README.md
├── zero-phoenix
├── web
│ ├── views
│ │ ├── layout_view.ex
│ │ ├── page_view.ex
│ │ ├── error_view.ex
│ │ ├── person_view.ex
│ │ ├── changeset_view.ex
│ │ └── error_helpers.ex
│ ├── controllers
│ │ ├── page_controller.ex
│ │ └── person_controller.ex
│ ├── resolvers
│ │ └── person_resolver.ex
│ ├── graphql
│ │ ├── schema.ex
│ │ └── types
│ │ │ └── person.ex
│ ├── models
│ │ ├── friendship.ex
│ │ └── person.ex
│ ├── gettext.ex
│ ├── router.ex
│ ├── templates
│ │ ├── page
│ │ │ └── index.html.eex
│ │ └── layout
│ │ │ └── app.html.eex
│ ├── channels
│ │ └── user_socket.ex
│ └── web.ex
├── lib
│ ├── zero_phoenix
│ │ ├── repo.ex
│ │ └── endpoint.ex
│ └── zero_phoenix.ex
├── test
│ ├── test_helper.exs
│ ├── views
│ │ ├── page_view_test.exs
│ │ ├── layout_view_test.exs
│ │ └── error_view_test.exs
│ ├── controllers
│ │ ├── page_controller_test.exs
│ │ └── person_controller_test.exs
│ ├── models
│ │ ├── friendship_test.exs
│ │ └── person_test.exs
│ └── support
│ │ ├── channel_case.ex
│ │ ├── conn_case.ex
│ │ └── model_case.ex
├── priv
│ ├── static
│ │ ├── favicon.ico
│ │ ├── images
│ │ │ └── phoenix.png
│ │ ├── robots.txt
│ │ └── js
│ │ │ ├── app.js
│ │ │ └── phoenix.js
│ ├── repo
│ │ ├── migrations
│ │ │ ├── 20160730004705_create_person.exs
│ │ │ └── 20160730024335_create_friendship.exs
│ │ └── seeds.exs
│ └── gettext
│ │ ├── en
│ │ └── LC_MESSAGES
│ │ │ └── errors.po
│ │ └── errors.pot
├── .gitignore
├── config
│ ├── test.exs
│ ├── config.exs
│ ├── dev.exs
│ └── prod.exs
├── mix.exs
├── mix.lock
└── README.md
└── README.md
/zero-rails/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-django/people/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-django/zero_django/models.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/test/fixtures/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-django/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 |
--------------------------------------------------------------------------------
/zero-django/.python-version:
--------------------------------------------------------------------------------
1 | 3.5.0
2 |
--------------------------------------------------------------------------------
/zero-django/zero_django/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-node/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/zero-rails/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-django/people/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/app/assets/javascripts/channels/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zero-rails/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/zero-scala/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.11
2 |
--------------------------------------------------------------------------------
/zero-rails/app/helpers/people_helper.rb:
--------------------------------------------------------------------------------
1 | module PeopleHelper
2 | end
3 |
--------------------------------------------------------------------------------
/zero-rails/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/zero-scala/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.8.0")
--------------------------------------------------------------------------------
/zero-django/requirements.txt:
--------------------------------------------------------------------------------
1 | django>=1.9
2 | graphene[django]
3 | django_graphiql
4 |
--------------------------------------------------------------------------------
/zero-rails/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/zero-django/people/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/zero-django/people/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/zero-scala/.gitignore:
--------------------------------------------------------------------------------
1 | # folders
2 | .idea
3 | target
4 | lib
5 | classes
6 |
7 | # files
8 | *.iml
9 |
--------------------------------------------------------------------------------
/zero-django/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steveluscher/zero-to-graphql/HEAD/zero-django/db.sqlite3
--------------------------------------------------------------------------------
/zero-phoenix/web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.LayoutView do
2 | use ZeroPhoenix.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/zero-phoenix/web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PageView do
2 | use ZeroPhoenix.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/zero-phoenix/lib/zero_phoenix/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Repo do
2 | use Ecto.Repo, otp_app: :zero_phoenix
3 | end
4 |
--------------------------------------------------------------------------------
/zero-phoenix/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start
2 |
3 | Ecto.Adapters.SQL.Sandbox.mode(ZeroPhoenix.Repo, :manual)
4 |
5 |
--------------------------------------------------------------------------------
/zero-django/people/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class PeopleConfig(AppConfig):
5 | name = 'people'
6 |
--------------------------------------------------------------------------------
/zero-rails/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steveluscher/zero-to-graphql/HEAD/zero-phoenix/priv/static/favicon.ico
--------------------------------------------------------------------------------
/zero-phoenix/test/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PageViewTest do
2 | use ZeroPhoenix.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/zero-phoenix/test/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.LayoutViewTest do
2 | use ZeroPhoenix.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/static/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steveluscher/zero-to-graphql/HEAD/zero-phoenix/priv/static/images/phoenix.png
--------------------------------------------------------------------------------
/zero-rails/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../javascripts .js
3 | //= link_directory ../stylesheets .css
4 |
--------------------------------------------------------------------------------
/zero-rails/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/zero-rails/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/zero-rails/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/zero-scala/src/test/resources/application.conf:
--------------------------------------------------------------------------------
1 | memoryDb {
2 | url = "jdbc:h2:mem:testDb"
3 | driver = org.h2.Driver
4 | connectionPool = disabled
5 | keepAliveConnection = true
6 | }
--------------------------------------------------------------------------------
/zero-rails/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_zero-rails_session'
4 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/zero-phoenix/web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PageController do
2 | use ZeroPhoenix.Web, :controller
3 |
4 | def index(conn, _params) do
5 | render conn, "index.html"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/zero-django/people/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | url(r'^people/$', views.index),
7 | url(r'^people/([1-9][0-9]*)/$', views.show, name='person'),
8 | ]
9 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/per_form_csrf_tokens.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Enable per-form CSRF tokens.
4 | Rails.application.config.action_controller.per_form_csrf_tokens = true
5 |
--------------------------------------------------------------------------------
/zero-rails/app/controllers/people_controller.rb:
--------------------------------------------------------------------------------
1 | class PeopleController < ApplicationController
2 | def index
3 | render json: Person.all
4 | end
5 | def show
6 | render json: Person.find(params[:id])
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/zero-scala/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | memoryDb {
2 | url = "jdbc:h2:mem:testDb"
3 | driver = org.h2.Driver
4 | connectionPool = disabled
5 | keepAliveConnection = true
6 | }
7 |
8 | akka.http.server.idle-timeout = 10 minutes
--------------------------------------------------------------------------------
/zero-rails/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ApplicationController.renderer.defaults.merge!(
4 | # http_host: 'example.org',
5 | # https: false
6 | # )
7 |
--------------------------------------------------------------------------------
/zero-rails/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/zero-rails/app/models/person.rb:
--------------------------------------------------------------------------------
1 | class Person < ApplicationRecord
2 | has_and_belongs_to_many :friends,
3 | class_name: 'Person',
4 | join_table: :friendships,
5 | foreign_key: :person_id,
6 | association_foreign_key: :friend_id
7 | end
8 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/request_forgery_protection.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Enable origin-checking CSRF mitigation.
4 | Rails.application.config.action_controller.forgery_protection_origin_check = true
5 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/static/js/app.js:
--------------------------------------------------------------------------------
1 | // for phoenix_html support, including form and button helpers
2 | // copy the following scripts into your javascript bundle:
3 | // * https://raw.githubusercontent.com/phoenixframework/phoenix_html/v2.3.0/priv/static/phoenix_html.js
--------------------------------------------------------------------------------
/zero-rails/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/zero-rails/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
2 | module ApplicationCable
3 | class Channel < ActionCable::Channel::Base
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/zero-rails/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require_relative '../config/boot'
8 | require 'rake'
9 | Rake.application.run
10 |
--------------------------------------------------------------------------------
/zero-phoenix/test/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PageControllerTest do
2 | use ZeroPhoenix.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get conn, "/"
6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/zero-rails/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
2 | module ApplicationCable
3 | class Connection < ActionCable::Connection::Base
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/zero-rails/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :people, :only => [:index, :show]
3 | post '/graphql', to: 'graphql#query'
4 | if Rails.env.development?
5 | mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/zero-rails/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/zero-rails/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 |
5 | # Action Cable requires that all classes are loaded in advance
6 | Rails.application.eager_load!
7 |
8 | run Rails.application
9 |
--------------------------------------------------------------------------------
/zero-rails/app/controllers/graphql_controller.rb:
--------------------------------------------------------------------------------
1 | class GraphqlController < ApplicationController
2 | skip_before_filter :verify_authenticity_token
3 |
4 | def query
5 | result_hash = Schema.execute(params[:query], variables: params[:variables])
6 | render json: result_hash
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/zero-rails/config/cable.yml:
--------------------------------------------------------------------------------
1 | # Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket.
2 | production:
3 | adapter: redis
4 | url: redis://localhost:6379/1
5 |
6 | development:
7 | adapter: async
8 |
9 | test:
10 | adapter: async
11 |
--------------------------------------------------------------------------------
/zero-django/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zero_django.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/zero-rails/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../../config/application', __FILE__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/zero-rails/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/zero-rails/db/migrate/20160413032518_create_people.rb:
--------------------------------------------------------------------------------
1 | class CreatePeople < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :people do |t|
4 | t.string :first_name
5 | t.string :last_name
6 | t.string :email
7 | t.string :username
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/zero-rails/db/migrate/20160413033451_create_friendships.rb:
--------------------------------------------------------------------------------
1 | class CreateFriendships < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :friendships do |t|
4 | t.references :person, foreign_key: true
5 | t.references :friend, foreign_key: true
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/zero-phoenix/web/resolvers/person_resolver.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PersonResolver do
2 |
3 | alias ZeroPhoenix.Repo
4 |
5 | def find(%{id: id}, _info) do
6 | case ZeroPhoenix.Person |> Repo.get(id) do
7 | nil -> {:error, "Person id #{id} not found"}
8 | person -> {:ok, person}
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/zero-rails/app/serializers/person_serializer.rb:
--------------------------------------------------------------------------------
1 | class PersonSerializer < ActiveModel::Serializer
2 | attributes :id, :first_name, :last_name, :email, :username, :friends
3 | def id
4 | object.id.to_s
5 | end
6 | def friends
7 | object.friend_ids.map do |id|
8 | Rails.application.routes.url_helpers.person_path(id)
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/zero-rails/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
7 | fixtures :all
8 |
9 | # Add more helper methods to be used by all tests here...
10 | end
11 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/repo/migrations/20160730004705_create_person.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Repo.Migrations.CreatePerson do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:people) do
6 | add :first_name, :string
7 | add :last_name, :string
8 | add :username, :string
9 | add :email, :string
10 |
11 | timestamps()
12 | end
13 |
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/callback_terminator.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Do not halt callback chains when a callback returns false. This is a new
4 | # Rails 5.0 default, so it is introduced as a configuration option to ensure
5 | # that apps made with earlier versions of Rails are not affected when upgrading.
6 | ActiveSupport.halt_callback_chains_on_return_false = false
7 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/active_record_belongs_to_required_by_default.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Require `belongs_to` associations by default. This is a new Rails 5.0
4 | # default, so it is introduced as a configuration option to ensure that apps
5 | # made on earlier versions of Rails are not affected when upgrading.
6 | Rails.application.config.active_record.belongs_to_required_by_default = true
7 |
--------------------------------------------------------------------------------
/zero-phoenix/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generated on crash by the VM
8 | erl_crash.dump
9 |
10 | # The config/prod.secret.exs file by default contains sensitive
11 | # data and you should not commit it into version control.
12 | #
13 | # Alternatively, you may comment the line below and commit the
14 | # secrets file as long as you replace its contents by environment
15 | # variables.
16 | /config/prod.secret.exs
17 |
--------------------------------------------------------------------------------
/zero-django/zero_django/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url, include
2 | from django.contrib import admin
3 | from django.views.decorators.csrf import csrf_exempt
4 | from graphene_django.views import GraphQLView
5 |
6 |
7 | from .schema import schema
8 |
9 | urlpatterns = [
10 | url(r'^graphiql', include('django_graphiql.urls')),
11 | url(r'^graphql', csrf_exempt(GraphQLView.as_view(schema=schema))),
12 | url(r'^', include('people.urls')),
13 | ]
14 |
--------------------------------------------------------------------------------
/zero-rails/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ZeroRails
5 | <%= csrf_meta_tags %>
6 | <%= action_cable_meta_tag %>
7 |
8 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => 'reload' %>
9 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/zero-phoenix/web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.ErrorView do
2 | use ZeroPhoenix.Web, :view
3 |
4 | def render("404.html", _assigns) do
5 | "Page not found"
6 | end
7 |
8 | def render("500.html", _assigns) do
9 | "Internal server error"
10 | end
11 |
12 | # In case no render clause matches or no
13 | # template is found, let's render it as 500
14 | def template_not_found(_template, assigns) do
15 | render "500.html", assigns
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/zero-django/zero_django/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for zero_django project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zero_django.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/repo/migrations/20160730024335_create_friendship.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Repo.Migrations.CreateFriendship do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:friendships) do
6 | add :person_id, references(:people, on_delete: :nothing)
7 | add :friend_id, references(:people, on_delete: :nothing)
8 |
9 | timestamps()
10 | end
11 |
12 | create index(:friendships, [:person_id])
13 | create index(:friendships, [:friend_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/zero-rails/app/assets/javascripts/cable.coffee:
--------------------------------------------------------------------------------
1 | # Action Cable provides the framework to deal with WebSockets in Rails.
2 | # You can generate new channels where WebSocket features live using the rails generate channel command.
3 | #
4 | # Turn on the cable connection by removing the comments after the require statements (and ensure it's also on in config/routes.rb).
5 | #
6 | #= require action_cable
7 | #= require_self
8 | #= require_tree ./channels
9 | #
10 | # @App ||= {}
11 | # App.cable = ActionCable.createConsumer()
12 |
--------------------------------------------------------------------------------
/zero-phoenix/web/graphql/schema.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Graphql.Schema do
2 | use Absinthe.Schema
3 |
4 | import_types ZeroPhoenix.Graphql.Types.Person
5 |
6 | alias ZeroPhoenix.Repo
7 |
8 | query do
9 | field :person, type: :person do
10 | arg :id, non_null(:id)
11 | resolve fn %{id: id}, _info ->
12 | case ZeroPhoenix.Person |> Repo.get(id) do
13 | nil -> {:error, "Person id #{id} not found"}
14 | person -> {:ok, person}
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/zero-rails/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 | end
6 |
7 | ActionController::Renderers.add :json do |json, options|
8 | unless json.kind_of?(String)
9 | json = json.as_json(options) if json.respond_to?(:as_json)
10 | json = JSON.pretty_generate(json, options)
11 | end
12 | self.content_type ||= Mime::JSON
13 | json
14 | end
15 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/zero-rails/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m))
11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) }
12 | gem 'spring', match[1]
13 | require 'spring/binstub'
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/zero-phoenix/test/models/friendship_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.FriendshipTest do
2 | use ZeroPhoenix.ModelCase
3 |
4 | alias ZeroPhoenix.Friendship
5 |
6 | @valid_attrs %{friend_id: 42, person_id: 42}
7 | @invalid_attrs %{}
8 |
9 | test "changeset with valid attributes" do
10 | changeset = Friendship.changeset(%Friendship{}, @valid_attrs)
11 | assert changeset.valid?
12 | end
13 |
14 | test "changeset with invalid attributes" do
15 | changeset = Friendship.changeset(%Friendship{}, @invalid_attrs)
16 | refute changeset.valid?
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/zero-phoenix/web/models/friendship.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Friendship do
2 | use ZeroPhoenix.Web, :model
3 |
4 | @required_fields ~w(person_id friend_id)
5 |
6 | schema "friendships" do
7 | belongs_to :person, ZeroPhoenix.Person
8 | belongs_to :friend, ZeroPhoenix.Person
9 |
10 | timestamps()
11 | end
12 |
13 | @doc """
14 | Builds a changeset based on the `struct` and `params`.
15 | """
16 | def changeset(struct, params \\ %{}) do
17 | struct
18 | |> cast(params, @required_fields)
19 | |> validate_required(@required_fields)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/zero-rails/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module ZeroRails
10 | class Application < Rails::Application
11 | # Settings in config/environments/* take precedence over those specified here.
12 | # Application configuration should go into files in config/initializers
13 | # -- all .rb files in that directory are automatically loaded.
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/zero-phoenix/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :zero_phoenix, ZeroPhoenix.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
12 | # Configure your database
13 | config :zero_phoenix, ZeroPhoenix.Repo,
14 | adapter: Ecto.Adapters.Postgres,
15 | username: "postgres",
16 | password: "postgres",
17 | database: "zero_phoenix_test",
18 | hostname: "localhost",
19 | pool: Ecto.Adapters.SQL.Sandbox
20 |
--------------------------------------------------------------------------------
/zero-django/people/views.py:
--------------------------------------------------------------------------------
1 | from django.core import serializers
2 | from django.forms.models import model_to_dict
3 | from django.http import JsonResponse
4 |
5 | from .models import Person
6 |
7 | def index(request):
8 | people = {
9 | 'people': [person.as_json() for person in Person.objects.all()],
10 | }
11 | return JsonResponse(people, json_dumps_params={'indent': 2}, safe=False)
12 |
13 | def show(request, person_id):
14 | person = {
15 | 'person': Person.objects.get(pk=person_id).as_json(),
16 | }
17 | return JsonResponse(person, json_dumps_params={'indent': 2}, safe=False)
18 |
--------------------------------------------------------------------------------
/zero-phoenix/test/models/person_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PersonTest do
2 | use ZeroPhoenix.ModelCase
3 |
4 | alias ZeroPhoenix.Person
5 |
6 | @valid_attrs %{"email": "some content", first_name: "some content", last_name: "some content", username: "some content"}
7 | @invalid_attrs %{}
8 |
9 | test "changeset with valid attributes" do
10 | changeset = Person.changeset(%Person{}, @valid_attrs)
11 | assert changeset.valid?
12 | end
13 |
14 | test "changeset with invalid attributes" do
15 | changeset = Person.changeset(%Person{}, @invalid_attrs)
16 | refute changeset.valid?
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/zero-rails/README.md:
--------------------------------------------------------------------------------
1 | # Rails example
2 |
3 | ## Prerequisites
4 |
5 | * Ruby >=2.3.0
6 |
7 | ```
8 | # Install with RVM (https://rvm.io/rvm/install)
9 | \curl -sSL https://get.rvm.io | bash -s stable --ruby=2.3
10 | ```
11 |
12 | * Bundler >=1.11.2
13 |
14 | ```
15 | # Install with `gem install`
16 | gem install bundler
17 | ```
18 |
19 | ## Installation
20 |
21 | cd zero-rails
22 | bundle
23 |
24 | ## Seed the database
25 |
26 | rails db:migrate
27 | rails db:seed
28 |
29 | ## Running the example
30 |
31 | rails server
32 |
33 | Visit http://localhost:3000/graphiql
34 |
--------------------------------------------------------------------------------
/zero-phoenix/web/views/person_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PersonView do
2 | use ZeroPhoenix.Web, :view
3 |
4 | def render("index.json", %{people: people}) do
5 | %{data: render_many(people, ZeroPhoenix.PersonView, "person.json")}
6 | end
7 |
8 | def render("show.json", %{person: person}) do
9 | %{data: render_one(person, ZeroPhoenix.PersonView, "person.json")}
10 | end
11 |
12 | def render("person.json", %{person: person}) do
13 | %{id: person.id,
14 | first_name: person.first_name,
15 | last_name: person.last_name,
16 | username: person.username,
17 | email: person. email}
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/zero-rails/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | # Ignore Byebug command history file.
21 | .byebug_history
22 |
--------------------------------------------------------------------------------
/zero-django/people/migrations/0002_person_friends.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9.5 on 2016-04-12 03:51
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('people', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='person',
18 | name='friends',
19 | field=models.ManyToManyField(related_name='_person_friends_+', to=settings.AUTH_USER_MODEL),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/zero-phoenix/web/views/changeset_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.ChangesetView do
2 | use ZeroPhoenix.Web, :view
3 |
4 | @doc """
5 | Traverses and translates changeset errors.
6 |
7 | See `Ecto.Changeset.traverse_errors/2` and
8 | `ZeroPhoenix.ErrorHelpers.translate_error/1` for more details.
9 | """
10 | def translate_errors(changeset) do
11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
12 | end
13 |
14 | def render("error.json", %{changeset: changeset}) do
15 | # When encoded, the changeset returns its errors
16 | # as a JSON object. So we just pass it forward.
17 | %{errors: translate_errors(changeset)}
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/zero-rails/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/zero-django/README.md:
--------------------------------------------------------------------------------
1 | # Django example
2 |
3 | ## Prerequisites
4 |
5 | * Xcode Command Line Tools
6 |
7 | ```
8 | # Download from (https://developer.apple.com/xcode/download/)
9 | xcode-select --install
10 | ```
11 |
12 | * Python >=3.5.0
13 |
14 | ```
15 | # Install pyenv with Homebrew (https://github.com/yyuu/pyenv#homebrew-on-mac-os-x)
16 | brew install pyenv
17 | # Install Python 3 with pyenv
18 | cd zero-django
19 | pyenv install
20 | ```
21 |
22 | ## Installation
23 |
24 | cd zero-django
25 | pip install -r requirements.txt
26 |
27 | ## Running the example
28 |
29 | ./manage.py runserver
30 |
31 | Visit http://localhost:8000/graphiql
32 |
--------------------------------------------------------------------------------
/zero-node/README.md:
--------------------------------------------------------------------------------
1 | # Node example
2 |
3 | The GraphQL schema in this example resolves data by fetching it via HTTP from the REST-ful endpoints of the Django example. This should give you an idea of how to wrap one or more existing APIs (REST, Redis, Thrift, ZeroMQ, et cetera) to expose one unified GraphQL endpoint through which your entire universe of data is accessible.
4 |
5 | ## Prerequisites
6 |
7 | * Node >=4.2.3 (Download from https://nodejs.org/en/download/)
8 |
9 | ## Installation
10 |
11 | cd zero-node
12 | npm install
13 |
14 | ## Running the example
15 |
16 | # Follow the instructions to start the Node server, then...
17 | npm start
18 |
19 | Visit http://localhost:5000/graphiql
20 |
--------------------------------------------------------------------------------
/zero-phoenix/test/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.ErrorViewTest do
2 | use ZeroPhoenix.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(ZeroPhoenix.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(ZeroPhoenix.ErrorView, "500.html", []) ==
14 | "Internal server error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(ZeroPhoenix.ErrorView, "505.html", []) ==
19 | "Internal server error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/zero-rails/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/zero-rails/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/zero-django/people/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import AbstractUser
2 | from django.core.urlresolvers import reverse
3 | from django.db import models
4 |
5 | class Person(AbstractUser):
6 | class Meta:
7 | app_label = 'people'
8 | db_table = 'person'
9 |
10 | friends = models.ManyToManyField('self')
11 |
12 | def as_json(self):
13 | out = dict(
14 | id=str(self.id),
15 | first_name=self.first_name,
16 | last_name=self.last_name,
17 | email=self.email,
18 | username=self.username,
19 | friends = [
20 | reverse('person', args=[friend.id])
21 | for friend in self.friends.all()
22 | ],
23 | )
24 | return out
25 |
--------------------------------------------------------------------------------
/zero-scala/build.sbt:
--------------------------------------------------------------------------------
1 | name := "zero-to-graphql"
2 | description := "An examples of GraphQL endpoint created using sangria and akka-http"
3 |
4 | scalaVersion := "2.12.1"
5 | scalacOptions ++= Seq("-deprecation", "-feature")
6 |
7 | libraryDependencies ++= Seq(
8 | // GraphQL
9 | "org.sangria-graphql" %% "sangria" % "1.1.0",
10 | "org.sangria-graphql" %% "sangria-spray-json" % "1.0.0",
11 |
12 | // Http
13 | "com.typesafe.akka" %% "akka-http" % "10.0.4",
14 | "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.4",
15 |
16 | // Database
17 | "com.typesafe.slick" %% "slick" % "3.2.0",
18 | "com.h2database" % "h2" % "1.4.193",
19 | "org.slf4j" % "slf4j-nop" % "1.7.21",
20 |
21 | // Testing
22 | "org.scalatest" %% "scalatest" % "3.0.1" % Test
23 | )
24 |
25 | Revolver.settings
--------------------------------------------------------------------------------
/zero-phoenix/web/models/person.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Person do
2 | use ZeroPhoenix.Web, :model
3 |
4 | @required_fields ~w(first_name last_name username email)
5 |
6 | schema "people" do
7 | field :first_name, :string
8 | field :last_name, :string
9 | field :username, :string
10 | field :email, :string
11 |
12 | has_many :friendships, ZeroPhoenix.Friendship #, on_delete: :delete_all
13 | has_many :friends, through: [:friendships, :friend] #, on_delete: :delete_all
14 |
15 | timestamps()
16 | end
17 |
18 | @doc """
19 | Builds a changeset based on the `struct` and `params`.
20 | """
21 | def changeset(struct, params \\ %{}) do
22 | struct
23 | |> cast(params, @required_fields)
24 | |> validate_required(@required_fields)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/zero-node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zero-node",
3 | "version": "1.0.0",
4 | "description": "An example of how to write a GraphQL schema that resolves data from a REST API",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon --exec babel-node --presets es2015,stage-0 -- index.js",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "Steven Luscher",
11 | "license": "MIT",
12 | "dependencies": {
13 | "babel-cli": "6.7.5",
14 | "babel-preset-es2015": "6.6.0",
15 | "babel-preset-stage-0": "6.5.0",
16 | "dataloader": "1.2.0",
17 | "express": "4.13.4",
18 | "express-graphql": "0.5.1",
19 | "graphql": "0.5.0",
20 | "graphql-relay": "0.4.0",
21 | "node-fetch": "1.5.1",
22 | "nodemon": "1.9.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/zero-phoenix/web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import ZeroPhoenix.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :zero_phoenix
24 | end
25 |
--------------------------------------------------------------------------------
/zero-rails/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/zero-rails/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file. JavaScript code in this file should be added after the last require_* statement.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require turbolinks
16 | //= require_tree .
17 |
--------------------------------------------------------------------------------
/zero-phoenix/web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Router do
2 | use ZeroPhoenix.Web, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | end
11 |
12 | pipeline :api do
13 | plug :accepts, ["json"]
14 | end
15 |
16 | scope "/", ZeroPhoenix do
17 | pipe_through :browser # Use the default browser stack
18 |
19 | get "/", PageController, :index
20 | end
21 |
22 | scope "/api", ZeroPhoenix do
23 | pipe_through :api
24 |
25 | resources "/people", PersonController, except: [:new, :edit]
26 | end
27 |
28 | scope "/graphiql" do
29 | pipe_through :api
30 |
31 | forward "/", Absinthe.Plug.GraphiQL, schema: ZeroPhoenix.Graphql.Schema, interface: :simple
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/zero-phoenix/web/graphql/types/person.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Graphql.Types.Person do
2 | use Absinthe.Schema.Notation
3 |
4 | import Ecto
5 |
6 | alias ZeroPhoenix.Repo
7 |
8 | @desc "a person"
9 | object :person do
10 | @desc "unique identifier for the person"
11 | field :id, non_null(:string)
12 |
13 | @desc "first name of a person"
14 | field :first_name, non_null(:string)
15 |
16 | @desc "last name of a person"
17 | field :last_name, non_null(:string)
18 |
19 | @desc "username of a person"
20 | field :username, non_null(:string)
21 |
22 | @desc "email of a person"
23 | field :email, non_null(:string)
24 |
25 | @desc "a list of friends for our person"
26 | field :friends, list_of(:person) do
27 | resolve fn _, %{source: person} ->
28 | {:ok, Repo.all(assoc(person, :friends))}
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/zero-rails/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a way to update your development environment automatically.
15 | # Add necessary update steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system 'bundle check' or system! 'bundle install'
20 |
21 | puts "\n== Updating database =="
22 | system! 'bin/rails db:migrate'
23 |
24 | puts "\n== Removing old logs and tempfiles =="
25 | system! 'bin/rails log:clear tmp:clear'
26 |
27 | puts "\n== Restarting application server =="
28 | system! 'bin/rails restart'
29 | end
30 |
--------------------------------------------------------------------------------
/zero-rails/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
7 | # Character.create(name: 'Luke', movie: movies.first)
8 | me = Person.create(first_name: 'Steven', last_name: 'Luscher', email: 'steveluscher@fb.com', username: 'steveluscher')
9 | dhh = Person.create(first_name: 'David', last_name: 'Heinemeier Hansson', email: 'dhh@37signals.com', username: 'dhh')
10 | ezra = Person.create(first_name: 'Ezra', last_name: 'Zygmuntowicz', email: 'ezra@merbivore.com', username: 'ezra')
11 | matz = Person.create(first_name: 'Yukihiro', last_name: 'Matsumoto', email: 'matz@heroku.com', username: 'matz')
12 | me.friends << [matz]
13 | dhh.friends << [ezra, matz]
14 | ezra.friends << [dhh, matz]
15 | matz.friends << [me, ezra, dhh]
16 |
--------------------------------------------------------------------------------
/zero-rails/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 734ef9e932795d0f3bb85a0ecb4047c874947c874a40b709aa778714753b7fab8c67c5cd2653f62872d30231a6290ae848eed6b60ab15c1e65453a2efd71a401
15 |
16 | test:
17 | secret_key_base: 12ff9caea124ac3e11b27154623a2f62e32c2bba036efe6ae38d1abcafa7726559f26246d8e6c9d46e872d6d9fc798156d9a29a967bc1e8f11bed10b7b5077b0
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/zero-rails/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a starting point to setup your application.
15 | # Add necessary setup steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') or system!('bundle install')
20 |
21 | # puts "\n== Copying sample files =="
22 | # unless File.exist?('config/database.yml')
23 | # cp 'config/database.yml.sample', 'config/database.yml'
24 | # end
25 |
26 | puts "\n== Preparing database =="
27 | system! 'bin/rails db:setup'
28 |
29 | puts "\n== Removing old logs and tempfiles =="
30 | system! 'bin/rails log:clear tmp:clear'
31 |
32 | puts "\n== Restarting application server =="
33 | system! 'bin/rails restart'
34 | end
35 |
--------------------------------------------------------------------------------
/zero-phoenix/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # General application configuration
9 | config :zero_phoenix,
10 | ecto_repos: [ZeroPhoenix.Repo]
11 |
12 | # Configures the endpoint
13 | config :zero_phoenix, ZeroPhoenix.Endpoint,
14 | url: [host: "localhost"],
15 | secret_key_base: "bS5peykLEJ5cQDZ+u5M+ncgdhOyKND6P/vSLoLKdqaRgGqe1QHuejc5XvfifUUUo",
16 | render_errors: [view: ZeroPhoenix.ErrorView, accepts: ~w(html json)],
17 | pubsub: [name: ZeroPhoenix.PubSub,
18 | adapter: Phoenix.PubSub.PG2]
19 |
20 | # Configures Elixir's Logger
21 | config :logger, :console,
22 | format: "$time $metadata[$level] $message\n",
23 | metadata: [:request_id]
24 |
25 | # Import environment specific config. This must remain at the bottom
26 | # of this file so it overrides the configuration defined above.
27 | import_config "#{Mix.env}.exs"
28 |
--------------------------------------------------------------------------------
/zero-phoenix/web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
<%= gettext "Welcome to %{name}", name: "Phoenix!" %>
3 |
A productive web framework that does not compromise speed and maintainability.
4 |
5 |
6 |
37 |
--------------------------------------------------------------------------------
/zero-phoenix/lib/zero_phoenix.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix do
2 | use Application
3 |
4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | # Define workers and child supervisors to be supervised
10 | children = [
11 | # Start the Ecto repository
12 | supervisor(ZeroPhoenix.Repo, []),
13 | # Start the endpoint when the application starts
14 | supervisor(ZeroPhoenix.Endpoint, []),
15 | # Start your own worker by calling: ZeroPhoenix.Worker.start_link(arg1, arg2, arg3)
16 | # worker(ZeroPhoenix.Worker, [arg1, arg2, arg3]),
17 | ]
18 |
19 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
20 | # for other strategies and supported options
21 | opts = [strategy: :one_for_one, name: ZeroPhoenix.Supervisor]
22 | Supervisor.start_link(children, opts)
23 | end
24 |
25 | # Tell Phoenix to update the endpoint configuration
26 | # whenever the application is updated.
27 | def config_change(changed, _new, removed) do
28 | ZeroPhoenix.Endpoint.config_change(changed, removed)
29 | :ok
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/zero-phoenix/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | alias ZeroPhoenix.Repo
24 | import Ecto
25 | import Ecto.Changeset
26 | import Ecto.Query
27 |
28 |
29 | # The default endpoint for testing
30 | @endpoint ZeroPhoenix.Endpoint
31 | end
32 | end
33 |
34 | setup tags do
35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ZeroPhoenix.Repo)
36 |
37 | unless tags[:async] do
38 | Ecto.Adapters.SQL.Sandbox.mode(ZeroPhoenix.Repo, {:shared, self()})
39 | end
40 |
41 | :ok
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/zero-rails/app/models/schema.rb:
--------------------------------------------------------------------------------
1 | PersonType = GraphQL::ObjectType.define do
2 | name 'Person'
3 | description 'Somebody to lean on'
4 |
5 | field :id, !types.ID
6 | field :firstName, !types.String, property: :first_name
7 | field :lastName, !types.String, property: :last_name
8 | field :email, !types.String, 'Like a phone number, but spammier'
9 | field :username, !types.String, 'Use this to log in to your computer'
10 | field :friends, -> { types[PersonType] }, 'Some people to lean on'
11 | field :fullName do
12 | type !types.String
13 | description 'Every name, all at once'
14 | resolve -> (obj, args, ctx) { "#{obj.first_name} #{obj.last_name}" }
15 | end
16 | end
17 |
18 | QueryType = GraphQL::ObjectType.define do
19 | name 'Query'
20 | description 'The root of all queries'
21 |
22 | field :allPeople do
23 | type types[PersonType]
24 | description 'Everyone in the Universe'
25 | resolve -> (obj, args, ctx) { Person.all }
26 | end
27 | field :person do
28 | type PersonType
29 | description 'The person associated with a given ID'
30 | argument :id, !types.ID
31 | resolve -> (obj, args, ctx) { Person.find(args[:id]) }
32 | end
33 | end
34 |
35 | Schema = GraphQL::Schema.new(
36 | query: QueryType,
37 | )
38 |
--------------------------------------------------------------------------------
/zero-phoenix/web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello ZeroPhoenix!
11 | ">
12 |
13 |
14 |
15 |
16 |
24 |
25 |
<%= get_flash(@conn, :info) %>
26 |
<%= get_flash(@conn, :error) %>
27 |
28 |
29 | <%= render @view_module, @view_template, assigns %>
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/zero-phoenix/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 |
23 | alias ZeroPhoenix.Repo
24 | import Ecto
25 | import Ecto.Changeset
26 | import Ecto.Query
27 |
28 | import ZeroPhoenix.Router.Helpers
29 |
30 | # The default endpoint for testing
31 | @endpoint ZeroPhoenix.Endpoint
32 | end
33 | end
34 |
35 | setup tags do
36 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ZeroPhoenix.Repo)
37 |
38 | unless tags[:async] do
39 | Ecto.Adapters.SQL.Sandbox.mode(ZeroPhoenix.Repo, {:shared, self()})
40 | end
41 |
42 | {:ok, conn: Phoenix.ConnTest.build_conn()}
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/zero-phoenix/web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | # channel "room:*", ZeroPhoenix.RoomChannel
6 |
7 | ## Transports
8 | transport :websocket, Phoenix.Transports.WebSocket
9 | # transport :longpoll, Phoenix.Transports.LongPoll
10 |
11 | # Socket params are passed from the client and can
12 | # be used to verify and authenticate a user. After
13 | # verification, you can put default assigns into
14 | # the socket that will be set for all channels, ie
15 | #
16 | # {:ok, assign(socket, :user_id, verified_user_id)}
17 | #
18 | # To deny connection, return `:error`.
19 | #
20 | # See `Phoenix.Token` documentation for examples in
21 | # performing token verification on connect.
22 | def connect(_params, socket) do
23 | {:ok, socket}
24 | end
25 |
26 | # Socket id's are topics that allow you to identify all sockets for a given user:
27 | #
28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}"
29 | #
30 | # Would allow you to broadcast a "disconnect" event and terminate
31 | # all active sockets and channels for a given user:
32 | #
33 | # ZeroPhoenix.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
34 | #
35 | # Returning `nil` makes this socket anonymous.
36 | def id(_socket), do: nil
37 | end
38 |
--------------------------------------------------------------------------------
/zero-django/zero_django/schema.py:
--------------------------------------------------------------------------------
1 | import graphene
2 |
3 | from people.models import Person
4 |
5 | class PersonType(graphene.ObjectType):
6 | email = graphene.String(description='Like a phone number, but often longer')
7 | first_name = graphene.String()
8 | friends = graphene.List(lambda: PersonType, description='Mostly less strange people')
9 | full_name = graphene.String(description='Pretty much all of your name')
10 | id = graphene.String()
11 | last_name = graphene.String()
12 | username = graphene.String(description='Something you forget often')
13 |
14 | def resolve_friends(self, args, context, info):
15 | return self.friends.all()
16 | def resolve_full_name(self, args, context, info):
17 | return '{} {}'.format(self.first_name, self.last_name)
18 |
19 | class QueryType(graphene.ObjectType):
20 | all_people = graphene.List(PersonType, description='A few billion people')
21 | person = graphene.Field(
22 | PersonType,
23 | id=graphene.ID(),
24 | description='Just one person belonging to an ID',
25 | )
26 |
27 | def resolve_all_people(self, args, context, info):
28 | return Person.objects.all()
29 | def resolve_person(self, args, context, info):
30 | id = args.get('id')
31 | return Person.objects.get(pk=id)
32 |
33 | schema = graphene.Schema(query=QueryType)
34 |
--------------------------------------------------------------------------------
/zero-phoenix/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :zero_phoenix, ZeroPhoenix.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | watchers: []
15 |
16 |
17 | # Watch static and templates for browser reloading.
18 | config :zero_phoenix, ZeroPhoenix.Endpoint,
19 | live_reload: [
20 | patterns: [
21 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
22 | ~r{priv/gettext/.*(po)$},
23 | ~r{web/views/.*(ex)$},
24 | ~r{web/templates/.*(eex)$}
25 | ]
26 | ]
27 |
28 | # Do not include metadata nor timestamps in development logs
29 | config :logger, :console, format: "[$level] $message\n"
30 |
31 | # Set a higher stacktrace during development. Avoid configuring such
32 | # in production as building large stacktraces may be expensive.
33 | config :phoenix, :stacktrace_depth, 20
34 |
35 | # Configure your database
36 | config :zero_phoenix, ZeroPhoenix.Repo,
37 | adapter: Ecto.Adapters.Postgres,
38 | username: "postgres",
39 | password: "postgres",
40 | database: "zero_phoenix_dev",
41 | hostname: "localhost",
42 | pool_size: 10
43 |
--------------------------------------------------------------------------------
/zero-phoenix/lib/zero_phoenix/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :zero_phoenix
3 |
4 | socket "/socket", ZeroPhoenix.UserSocket
5 |
6 | # Serve at "/" the static files from "priv/static" directory.
7 | #
8 | # You should set gzip to true if you are running phoenix.digest
9 | # when deploying your static files in production.
10 | plug Plug.Static,
11 | at: "/", from: :zero_phoenix, gzip: false,
12 | only: ~w(css fonts images js favicon.ico robots.txt)
13 |
14 | # Code reloading can be explicitly enabled under the
15 | # :code_reloader configuration of your endpoint.
16 | if code_reloading? do
17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
18 | plug Phoenix.LiveReloader
19 | plug Phoenix.CodeReloader
20 | end
21 |
22 | plug Plug.RequestId
23 | plug Plug.Logger
24 |
25 | plug Plug.Parsers,
26 | parsers: [:urlencoded, :multipart, :json],
27 | pass: ["*/*"],
28 | json_decoder: Poison
29 |
30 | plug Plug.MethodOverride
31 | plug Plug.Head
32 |
33 | # The session will be stored in the cookie and signed,
34 | # this means its contents can be read but not tampered with.
35 | # Set :encryption_salt if you would also like to encrypt it.
36 | plug Plug.Session,
37 | store: :cookie,
38 | key: "_zero_phoenix_key",
39 | signing_salt: "QXaFyZw2"
40 |
41 | plug ZeroPhoenix.Router
42 | end
43 |
--------------------------------------------------------------------------------
/zero-phoenix/web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | if error = form.errors[field] do
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # Because error messages were defined within Ecto, we must
22 | # call the Gettext module passing our Gettext backend. We
23 | # also use the "errors" domain as translations are placed
24 | # in the errors.po file.
25 | # Ecto will pass the :count keyword if the error message is
26 | # meant to be pluralized.
27 | # On your own code and templates, depending on whether you
28 | # need the message to be pluralized or not, this could be
29 | # written simply as:
30 | #
31 | # dngettext "errors", "1 file", "%{count} files", count
32 | # dgettext "errors", "is invalid"
33 | #
34 | if count = opts[:count] do
35 | Gettext.dngettext(ZeroPhoenix.Gettext, "errors", msg, msg, count, opts)
36 | else
37 | Gettext.dgettext(ZeroPhoenix.Gettext, "errors", msg, opts)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # zero-to-graphql
2 |
3 | In this repository, you will find examples of GraphQL endpoints created using various languages atop different frameworks. The goal is to demonstrate how you might go about creating a GraphQL endpoint atop your *existing* infrastructure, whatever that may be, without having to rewrite your data model.
4 |
5 | ## Watch the original presentation
6 |
7 | [](https://youtu.be/UBGzsb2UkeY)
8 |
9 | ## The examples' data model
10 |
11 | Every example in this repository exposes a `Person` data model using an API considered idiomatic for the framework in question (eg. ActiveRecord for Rails). The type definition of the `Person` model looks like this:
12 |
13 | type Person {
14 | id: String!
15 | first_name: String!
16 | last_name: String!
17 | username: String!
18 | email: String!
19 | friends: [Person]
20 | }
21 |
22 | ## Running the examples
23 |
24 | Each example features its own `README.md` file to help you get up and running.
25 |
26 | ## Contributing
27 |
28 | See a language or framework for which there is no example? Feel free to send us a pull request! Expose the data model outlined above using whatever API you like, be sure to provide some seed data, and write a `README` that outlines all of the steps needed to get up and running with an instance of GraphiQL that you can use to issue queries to your new GraphQL endpoint.
29 |
--------------------------------------------------------------------------------
/zero-rails/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20160413033451) do
15 |
16 | create_table "friendships", force: :cascade do |t|
17 | t.integer "person_id"
18 | t.integer "friend_id"
19 | t.datetime "created_at", null: false
20 | t.datetime "updated_at", null: false
21 | end
22 |
23 | add_index "friendships", ["friend_id"], name: "index_friendships_on_friend_id"
24 | add_index "friendships", ["person_id"], name: "index_friendships_on_person_id"
25 |
26 | create_table "people", force: :cascade do |t|
27 | t.string "first_name"
28 | t.string "last_name"
29 | t.string "email"
30 | t.string "username"
31 | t.datetime "created_at", null: false
32 | t.datetime "updated_at", null: false
33 | end
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/zero-node/index.js:
--------------------------------------------------------------------------------
1 | import DataLoader from 'dataloader';
2 |
3 | import express from 'express';
4 | import fetch from 'node-fetch';
5 | import graphqlHTTP from 'express-graphql';
6 | import schema from './schema';
7 |
8 | const BASE_URL = 'http://localhost:8000';
9 |
10 | function getJSONFromRelativeURL(relativeURL) {
11 | return fetch(`${BASE_URL}${relativeURL}`)
12 | .then(res => res.json());
13 | }
14 |
15 | function getPeople() {
16 | return getJSONFromRelativeURL('/people/')
17 | .then(json => json.people);
18 | }
19 |
20 | function getPerson(id) {
21 | return getPersonByURL(`/people/${id}/`);
22 | }
23 |
24 | function getPersonByURL(relativeURL) {
25 | return getJSONFromRelativeURL(relativeURL)
26 | .then(json => json.person);
27 | }
28 |
29 | const app = express();
30 |
31 | app.use(graphqlHTTP(req => {
32 | const cacheMap = new Map();
33 | const peopleLoader =
34 | new DataLoader(keys => Promise.all(keys.map(getPeople)), {cacheMap});
35 | const personLoader =
36 | new DataLoader(keys => Promise.all(keys.map(getPerson)), {
37 | cacheKeyFn: key => `/people/${key}/`,
38 | cacheMap,
39 | });
40 | const personByURLLoader =
41 | new DataLoader(keys => Promise.all(keys.map(getPersonByURL)), {cacheMap});
42 | personLoader.loadAll = peopleLoader.load.bind(peopleLoader, '__all__');
43 | personLoader.loadByURL = personByURLLoader.load.bind(personByURLLoader);
44 | personLoader.loadManyByURL =
45 | personByURLLoader.loadMany.bind(personByURLLoader);
46 | const loaders = {person: personLoader};
47 | return {
48 | context: {loaders},
49 | graphiql: true,
50 | schema,
51 | };
52 | }));
53 |
54 | app.listen(
55 | 5000,
56 | () => console.log('GraphQL Server running at http://localhost:5000')
57 | );
58 |
--------------------------------------------------------------------------------
/zero-rails/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/zero-phoenix/web/controllers/person_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PersonController do
2 | use ZeroPhoenix.Web, :controller
3 |
4 | alias ZeroPhoenix.Person
5 |
6 | def index(conn, _params) do
7 | people = Repo.all(Person)
8 | render(conn, "index.json", people: people)
9 | end
10 |
11 | def create(conn, %{"person" => person_params}) do
12 | changeset = Person.changeset(%Person{}, person_params)
13 |
14 | case Repo.insert(changeset) do
15 | {:ok, person} ->
16 | conn
17 | |> put_status(:created)
18 | |> put_resp_header("location", person_path(conn, :show, person))
19 | |> render("show.json", person: person)
20 | {:error, changeset} ->
21 | conn
22 | |> put_status(:unprocessable_entity)
23 | |> render(ZeroPhoenix.ChangesetView, "error.json", changeset: changeset)
24 | end
25 | end
26 |
27 | def show(conn, %{"id" => id}) do
28 | person = Repo.get!(Person, id)
29 | render(conn, "show.json", person: person)
30 | end
31 |
32 | def update(conn, %{"id" => id, "person" => person_params}) do
33 | person = Repo.get!(Person, id)
34 | changeset = Person.changeset(person, person_params)
35 |
36 | case Repo.update(changeset) do
37 | {:ok, person} ->
38 | render(conn, "show.json", person: person)
39 | {:error, changeset} ->
40 | conn
41 | |> put_status(:unprocessable_entity)
42 | |> render(ZeroPhoenix.ChangesetView, "error.json", changeset: changeset)
43 | end
44 | end
45 |
46 | def delete(conn, %{"id" => id}) do
47 | person = Repo.get!(Person, id)
48 |
49 | # Here we use delete! (with a bang) because we expect
50 | # it to always work (and if it does not, it will raise).
51 | Repo.delete!(person)
52 |
53 | send_resp(conn, :no_content, "")
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/zero-rails/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/zero-scala/src/main/scala/SchemaDefinition.scala:
--------------------------------------------------------------------------------
1 | import sangria.schema._
2 | import sangria.macros.derive._
3 | import sangria.execution.deferred._
4 |
5 | object SchemaDefinition {
6 | def constantComplexity[Ctx](complexity: Double) =
7 | Some((_: Ctx, _: Args, child: Double) ⇒ child + complexity)
8 |
9 | val friend = Relation[Person, (Seq[String], Person), String]("friend", _._1, _._2)
10 |
11 | val personFetcher = Fetcher.relCaching(
12 | (repo: Repository, ids: Seq[String]) ⇒ repo.people(ids),
13 | (repo: Repository, ids: RelationIds[Person]) ⇒ repo.findFriends(ids(friend))
14 | )(HasId(_.id))
15 |
16 | val PersonType: ObjectType[Unit, Person] = deriveObjectType(
17 | DocumentField("firstName", "What you yell at me"),
18 | DocumentField("lastName", "What you yell at me when I've been bad"),
19 | DocumentField("username", "Log in as this"),
20 | AddFields(
21 | Field("fullName", StringType,
22 | description = Some("A name sandwich"),
23 | resolve = c ⇒ s"${c.value.firstName} ${c.value.lastName}"),
24 | Field("friends", ListType(PersonType),
25 | description = Some("People who lent you money"),
26 | complexity = constantComplexity(50),
27 | resolve = c ⇒ personFetcher.deferRelSeq(friend, c.value.id))))
28 |
29 | val QueryType = ObjectType("Query", "The root of all... queries", fields[Repository, Unit](
30 | Field("allPeople", ListType(PersonType),
31 | description = Some("Everyone, everywhere"),
32 | complexity = constantComplexity(100),
33 | resolve = _.ctx.allPeople),
34 |
35 | Field("person", OptionType(PersonType),
36 | arguments = Argument("id", StringType) :: Nil,
37 | complexity = constantComplexity(10),
38 | resolve = c ⇒ personFetcher.deferOpt(c.arg[String]("id")))))
39 |
40 | val schema = Schema(QueryType)
41 | }
42 |
--------------------------------------------------------------------------------
/zero-rails/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/zero-rails/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => 'public, max-age=3600'
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 | config.action_mailer.perform_caching = false
31 |
32 | # Tell Action Mailer not to deliver emails to the real world.
33 | # The :test delivery method accumulates sent emails in the
34 | # ActionMailer::Base.deliveries array.
35 | config.action_mailer.delivery_method = :test
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/zero-phoenix/web/web.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Web do
2 | @moduledoc """
3 | A module that keeps using definitions for controllers,
4 | views and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use ZeroPhoenix.Web, :controller
9 | use ZeroPhoenix.Web, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below.
17 | """
18 |
19 | def model do
20 | quote do
21 | use Ecto.Schema
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 | end
27 | end
28 |
29 | def controller do
30 | quote do
31 | use Phoenix.Controller
32 |
33 | alias ZeroPhoenix.Repo
34 | import Ecto
35 | import Ecto.Query
36 |
37 | import ZeroPhoenix.Router.Helpers
38 | import ZeroPhoenix.Gettext
39 | end
40 | end
41 |
42 | def view do
43 | quote do
44 | use Phoenix.View, root: "web/templates"
45 |
46 | # Import convenience functions from controllers
47 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
48 |
49 | # Use all HTML functionality (forms, tags, etc)
50 | use Phoenix.HTML
51 |
52 | import ZeroPhoenix.Router.Helpers
53 | import ZeroPhoenix.ErrorHelpers
54 | import ZeroPhoenix.Gettext
55 | end
56 | end
57 |
58 | def router do
59 | quote do
60 | use Phoenix.Router
61 | end
62 | end
63 |
64 | def channel do
65 | quote do
66 | use Phoenix.Channel
67 |
68 | alias ZeroPhoenix.Repo
69 | import Ecto
70 | import Ecto.Query
71 | import ZeroPhoenix.Gettext
72 | end
73 | end
74 |
75 | @doc """
76 | When used, dispatch to the appropriate controller/view/etc.
77 | """
78 | defmacro __using__(which) when is_atom(which) do
79 | apply(__MODULE__, which, [])
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/zero-rails/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 |
4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
5 | gem 'rails', '>= 5.0.0.beta3', '< 5.1'
6 | # Use sqlite3 as the database for Active Record
7 | gem 'sqlite3'
8 | # Use Puma as the app server
9 | gem 'puma'
10 | # Use SCSS for stylesheets
11 | gem 'sass-rails', '~> 5.0'
12 | # Use Uglifier as compressor for JavaScript assets
13 | gem 'uglifier', '>= 1.3.0'
14 | # Use CoffeeScript for .coffee assets and views
15 | gem 'coffee-rails', '~> 4.1.0'
16 | # See https://github.com/rails/execjs#readme for more supported runtimes
17 | # gem 'therubyracer', platforms: :ruby
18 |
19 | # Use jquery as the JavaScript library
20 | gem 'jquery-rails'
21 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
22 | gem 'turbolinks', '~> 5.x'
23 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
24 | gem 'jbuilder', '~> 2.0'
25 | # Use Redis adapter to run Action Cable in production
26 | # gem 'redis', '~> 3.0'
27 | # Use ActiveModel has_secure_password
28 | # gem 'bcrypt', '~> 3.1.7'
29 |
30 | # Use Capistrano for deployment
31 | # gem 'capistrano-rails', group: :development
32 |
33 | # Help serialize models to JSON
34 | gem 'active_model_serializers'
35 |
36 | # GraphQL
37 | gem 'graphql'
38 | gem "graphiql-rails"
39 |
40 | group :development, :test do
41 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
42 | gem 'byebug'
43 | end
44 |
45 | group :development do
46 | # Access an IRB console on exception pages or by using <%= console %> in views
47 | gem 'web-console', '~> 3.0'
48 | gem 'listen', '~> 3.0.5'
49 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
50 | gem 'spring'
51 | gem 'spring-watcher-listen', '~> 2.0.0'
52 | end
53 |
54 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
55 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
56 |
--------------------------------------------------------------------------------
/zero-phoenix/test/support/model_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.ModelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | model tests.
5 |
6 | You may define functions here to be used as helpers in
7 | your model tests. See `errors_on/2`'s definition as reference.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias ZeroPhoenix.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import ZeroPhoenix.ModelCase
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ZeroPhoenix.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(ZeroPhoenix.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 |
38 | @doc """
39 | Helper for returning list of errors in a struct when given certain data.
40 |
41 | ## Examples
42 |
43 | Given a User schema that lists `:name` as a required field and validates
44 | `:password` to be safe, it would return:
45 |
46 | iex> errors_on(%User{}, %{password: "password"})
47 | [password: "is unsafe", name: "is blank"]
48 |
49 | You could then write your assertion like:
50 |
51 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"})
52 |
53 | You can also create the changeset manually and retrieve the errors
54 | field directly:
55 |
56 | iex> changeset = User.changeset(%User{}, password: "password")
57 | iex> {:password, "is unsafe"} in changeset.errors
58 | true
59 | """
60 | def errors_on(struct, data) do
61 | struct.__struct__.changeset(struct, data)
62 | |> Ecto.Changeset.traverse_errors(&ZeroPhoenix.ErrorHelpers.translate_error/1)
63 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/zero-scala/src/test/scala/SchemaSpec.scala:
--------------------------------------------------------------------------------
1 | import language.postfixOps
2 | import org.scalatest.{Matchers, WordSpec}
3 | import sangria.ast.Document
4 | import sangria.execution.Executor
5 | import sangria.execution.deferred.DeferredResolver
6 | import sangria.macros._
7 | import sangria.marshalling.sprayJson._
8 | import spray.json._
9 |
10 | import scala.concurrent.Await
11 | import scala.concurrent.ExecutionContext.Implicits.global
12 | import scala.concurrent.duration._
13 |
14 | import SchemaDefinition.personFetcher
15 |
16 | class SchemaSpec extends WordSpec with Matchers {
17 | "Schema" should {
18 | "return person by ID" in {
19 | val query =
20 | graphql"""
21 | query personWithFriends {
22 | person(id: "1001") {
23 | id
24 | firstName
25 | lastName
26 | fullName
27 | username
28 | email
29 | friends {
30 | id
31 | fullName
32 | }
33 | }
34 | }
35 | """
36 |
37 | executeQuery(query) should be (
38 | """
39 | {
40 | "data": {
41 | "person": {
42 | "id": "1001",
43 | "lastName": "Vasquez",
44 | "firstName": "Leslie",
45 | "fullName": "Leslie Vasquez",
46 | "username": "leslie.vasquez",
47 | "email": "eu@Duiscursusdiam.co.uk",
48 | "friends": [
49 | {"id": "1003", "fullName": "Jena Brady"},
50 | {"id": "1005", "fullName": "Alexandra Evans"}
51 | ]
52 | }
53 | }
54 | }
55 | """.parseJson)
56 | }
57 | }
58 |
59 | def executeQuery(query: Document) = {
60 | val repository = Repository.createDatabase()
61 |
62 | try {
63 | val futureResult = Executor.execute(SchemaDefinition.schema, query, repository,
64 | deferredResolver = DeferredResolver.fetchers(personFetcher))
65 |
66 | Await.result(futureResult, 10 seconds)
67 | } finally repository.close()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/zero-rails/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum, this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # The code in the `on_worker_boot` will be called if you are using
36 | # clustered mode by specifying a number of `workers`. After each worker
37 | # process is booted this block will be run, if you are using `preload_app!`
38 | # option you will want to use this block to reconnect to any threads
39 | # or connections that may have been created at application boot, Ruby
40 | # cannot share connections between processes.
41 | #
42 | # on_worker_boot do
43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
44 | # end
45 |
46 | # Allow puma to be restarted by `rails restart` command.
47 | plugin :tmp_restart
48 |
--------------------------------------------------------------------------------
/zero-scala/README.md:
--------------------------------------------------------------------------------
1 | ## Scala Example
2 |
3 | An example Scala [GraphQL](http://facebook.github.io/graphql/) server written with [akka-http](http://doc.akka.io/docs/akka-stream-and-http-experimental/current/scala/http/) and [sangria](https://github.com/sangria-graphql/sangria).
4 |
5 | ### Prerequisites
6 |
7 | * [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
8 | * [SBT](http://www.scala-sbt.org/download.html)
9 |
10 | ### Running the example
11 |
12 | ```bash
13 | sbt ~reStart
14 | ```
15 |
16 | SBT will automatically compile and restart the server whenever the source code changes.
17 |
18 | After the server is started you can run queries interactively using [GraphiQL](https://github.com/graphql/graphiql) by opening [http://localhost:8080](http://localhost:8080) in a browser.
19 |
20 | ### Database Configuration
21 |
22 | This example uses an in-memory [H2](http://www.h2database.com/html/main.html) SQL database. The schema and example data will be re-created every time server starts.
23 |
24 | If you would like to change the database configuration or use a different database, then please update `src/main/resources/application.conf`.
25 |
26 | ### Deferred values
27 |
28 | Queries like this one:
29 |
30 | ```js
31 | query AllPeopleWithFriends {
32 | allPeople {
33 | fullName
34 | friends {
35 | firstName
36 | lastName
37 | }
38 | }
39 | }
40 | ```
41 |
42 | …may result in N+1 database queries since the `friends` field is fetched for every person in a list. In order to solve this problem, the example uses [deferred values](http://sangria-graphql.org/learn/#deferred-values-and-resolver): the `friends` field returns a `FriendsDeferred` instance which is then given to `FriendsResolver` by the execution engine, enabling it to batch friend queries.
43 |
44 | ### Protection against malicious queries
45 |
46 | Due to the recursive nature of the GraphQL schema, it's possible for a client to send infinitely deep `friends` queries. As a safety measure, this example uses [query complexity analysis](http://sangria-graphql.org/learn/#protection-against-malicious-queries) and rejects all queries above a complexity threshold (`300` by default).
47 |
--------------------------------------------------------------------------------
/zero-phoenix/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :zero_phoenix,
6 | version: "0.0.1",
7 | elixir: "~> 1.5.0-dev",
8 | elixirc_paths: elixirc_paths(Mix.env),
9 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
10 | build_embedded: Mix.env == :prod,
11 | start_permanent: Mix.env == :prod,
12 | aliases: aliases(),
13 | deps: deps()]
14 | end
15 |
16 | # Configuration for the OTP application.
17 | #
18 | # Type `mix help compile.app` for more information.
19 | def application do
20 | [
21 | mod: {ZeroPhoenix, []},
22 | applications:
23 | [
24 | :phoenix,
25 | :phoenix_pubsub,
26 | :phoenix_html,
27 | :cowboy,
28 | :logger,
29 | :gettext,
30 | :phoenix_ecto,
31 | :postgrex,
32 | :absinthe_plug
33 | ]
34 | ]
35 | end
36 |
37 | # Specifies which paths to compile per environment.
38 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
39 | defp elixirc_paths(_), do: ["lib", "web"]
40 |
41 | # Specifies your project dependencies.
42 | #
43 | # Type `mix help deps` for examples and options.
44 | defp deps do
45 | [
46 | {:phoenix, "~> 1.2.0"},
47 | {:phoenix_pubsub, "~> 1.0"},
48 | {:ecto, github: "elixir-ecto/ecto", override: true},
49 | {:phoenix_ecto, "~> 3.0"},
50 | {:postgrex, ">= 0.0.0"},
51 | {:phoenix_html, "~> 2.6"},
52 | {:phoenix_live_reload, "~> 1.0", only: :dev},
53 | {:gettext, "~> 0.11"},
54 | {:cowboy, "~> 1.0"},
55 | {:absinthe_plug, "~> 1.3"}
56 | ]
57 | end
58 |
59 | # Aliases are shortcuts or tasks specific to the current project.
60 | # For example, to create, migrate and run the seeds file at once:
61 | #
62 | # $ mix ecto.setup
63 | #
64 | # See the documentation for `Mix` for more info on aliases.
65 | defp aliases do
66 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
67 | "ecto.reset": ["ecto.drop", "ecto.setup"],
68 | "ecto.seed": ["run priv/repo/seeds.exs"],
69 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # ZeroPhoenix.Repo.insert!(%ZeroPhoenix.SomeModel{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
13 | alias ZeroPhoenix.Repo
14 | alias ZeroPhoenix.Person
15 | alias ZeroPhoenix.Friendship
16 |
17 | # reset the datastore
18 | Repo.delete_all(Person)
19 |
20 | # insert people
21 | me = Repo.insert!(%Person{ first_name: "Steven", last_name: "Luscher", email: "steveluscher@fb.com", username: "steveluscher" })
22 | dhh = Repo.insert!(%Person{ first_name: "David", last_name: "Heinemeier Hansson", email: "dhh@37signals.com", username: "dhh" })
23 | ezra = Repo.insert!(%Person{ first_name: "Ezra", last_name: "Zygmuntowicz", email: "ezra@merbivore.com", username: "ezra" })
24 | matz = Repo.insert!(%Person{ first_name: "Yukihiro", last_name: "Matsumoto", email: "matz@heroku.com", username: "matz" })
25 |
26 | me
27 | |> Ecto.build_assoc(:friendships)
28 | |> Friendship.changeset( %{ person_id: me.id, friend_id: matz.id } )
29 | |> Repo.insert
30 |
31 | dhh
32 | |> Ecto.build_assoc(:friendships)
33 | |> Friendship.changeset( %{ person_id: dhh.id, friend_id: ezra.id } )
34 | |> Repo.insert
35 |
36 | dhh
37 | |> Ecto.build_assoc(:friendships)
38 | |> Friendship.changeset( %{ person_id: dhh.id, friend_id: matz.id } )
39 | |> Repo.insert
40 |
41 | ezra
42 | |> Ecto.build_assoc(:friendships)
43 | |> Friendship.changeset( %{ person_id: ezra.id, friend_id: dhh.id } )
44 | |> Repo.insert
45 |
46 | ezra
47 | |> Ecto.build_assoc(:friendships)
48 | |> Friendship.changeset( %{ person_id: ezra.id, friend_id: matz.id } )
49 | |> Repo.insert
50 |
51 | matz
52 | |> Ecto.build_assoc(:friendships)
53 | |> Friendship.changeset( %{ person_id: matz.id, friend_id: me.id } )
54 | |> Repo.insert
55 |
56 | matz
57 | |> Ecto.build_assoc(:friendships)
58 | |> Friendship.changeset( %{ person_id: matz.id, friend_id: ezra.id } )
59 | |> Repo.insert
60 |
61 | matz
62 | |> Ecto.build_assoc(:friendships)
63 | |> Friendship.changeset( %{ person_id: matz.id, friend_id: dhh.id } )
64 | |> Repo.insert
65 |
--------------------------------------------------------------------------------
/zero-phoenix/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we configure the host to read the PORT
4 | # from the system environment. Therefore, you will need
5 | # to set PORT=80 before running your server.
6 | #
7 | # You should also configure the url host to something
8 | # meaningful, we use this information when generating URLs.
9 | #
10 | # Finally, we also include the path to a manifest
11 | # containing the digested version of static files. This
12 | # manifest is generated by the mix phoenix.digest task
13 | # which you typically run after static files are built.
14 | config :zero_phoenix, ZeroPhoenix.Endpoint,
15 | http: [port: {:system, "PORT"}],
16 | url: [host: "example.com", port: 80],
17 | cache_static_manifest: "priv/static/manifest.json"
18 |
19 | # Do not print debug messages in production
20 | config :logger, level: :info
21 |
22 | # ## SSL Support
23 | #
24 | # To get SSL working, you will need to add the `https` key
25 | # to the previous section and set your `:url` port to 443:
26 | #
27 | # config :zero_phoenix, ZeroPhoenix.Endpoint,
28 | # ...
29 | # url: [host: "example.com", port: 443],
30 | # https: [port: 443,
31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
33 | #
34 | # Where those two env variables return an absolute path to
35 | # the key and cert in disk or a relative path inside priv,
36 | # for example "priv/ssl/server.key".
37 | #
38 | # We also recommend setting `force_ssl`, ensuring no data is
39 | # ever sent via http, always redirecting to https:
40 | #
41 | # config :zero_phoenix, ZeroPhoenix.Endpoint,
42 | # force_ssl: [hsts: true]
43 | #
44 | # Check `Plug.SSL` for all available options in `force_ssl`.
45 |
46 | # ## Using releases
47 | #
48 | # If you are doing OTP releases, you need to instruct Phoenix
49 | # to start the server for all endpoints:
50 | #
51 | # config :phoenix, :serve_endpoints, true
52 | #
53 | # Alternatively, you can configure exactly which server to
54 | # start per endpoint:
55 | #
56 | # config :zero_phoenix, ZeroPhoenix.Endpoint, server: true
57 | #
58 | # You will also need to set the application root to `.` in order
59 | # for the new static assets to be served after a hot upgrade:
60 | #
61 | # config :zero_phoenix, ZeroPhoenix.Endpoint, root: "."
62 |
63 | # Finally import the config/prod.secret.exs
64 | # which should be versioned separately.
65 | import_config "prod.secret.exs"
66 |
--------------------------------------------------------------------------------
/zero-rails/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | if Rails.root.join('tmp/caching-dev.txt').exist?
17 | config.action_controller.perform_caching = true
18 |
19 | config.action_mailer.perform_caching = false
20 |
21 | config.cache_store = :memory_store
22 | config.public_file_server.headers = {
23 | 'Cache-Control' => 'public, max-age=172800'
24 | }
25 | else
26 | config.action_controller.perform_caching = false
27 |
28 | config.action_mailer.perform_caching = false
29 |
30 | config.cache_store = :null_store
31 | end
32 |
33 | # Don't care if the mailer can't send.
34 | config.action_mailer.raise_delivery_errors = false
35 |
36 | # Print deprecation notices to the Rails logger.
37 | config.active_support.deprecation = :log
38 |
39 | # Raise an error on page load if there are pending migrations.
40 | config.active_record.migration_error = :page_load
41 |
42 | # Debug mode disables concatenation and preprocessing of assets.
43 | # This option may cause significant delays in view rendering with a large
44 | # number of complex assets.
45 | config.assets.debug = true
46 |
47 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
48 | # yet still be able to expire them through the digest params.
49 | config.assets.digest = true
50 |
51 | # Adds additional error checking when serving assets at runtime.
52 | # Checks for improperly declared sprockets dependencies.
53 | # Raises helpful error messages.
54 | config.assets.raise_runtime_errors = true
55 |
56 | # Raises error for missing translations
57 | # config.action_view.raise_on_missing_translations = true
58 |
59 | # Use an evented file watcher to asynchronously detect changes in source code,
60 | # routes, locales, etc. This feature depends on the listen gem.
61 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
62 | end
63 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
13 | ## From Ecto.Changeset.cast/4
14 | msgid "can't be blank"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.unique_constraint/3
18 | msgid "has already been taken"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.put_change/3
22 | msgid "is invalid"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_format/3
26 | msgid "has invalid format"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_subset/3
30 | msgid "has an invalid entry"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_exclusion/3
34 | msgid "is reserved"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_confirmation/3
38 | msgid "does not match confirmation"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.no_assoc_constraint/3
42 | msgid "is still associated to this entry"
43 | msgstr ""
44 |
45 | msgid "are still associated to this entry"
46 | msgstr ""
47 |
48 | ## From Ecto.Changeset.validate_length/3
49 | msgid "should be %{count} character(s)"
50 | msgid_plural "should be %{count} character(s)"
51 | msgstr[0] ""
52 | msgstr[1] ""
53 |
54 | msgid "should have %{count} item(s)"
55 | msgid_plural "should have %{count} item(s)"
56 | msgstr[0] ""
57 | msgstr[1] ""
58 |
59 | msgid "should be at least %{count} character(s)"
60 | msgid_plural "should be at least %{count} character(s)"
61 | msgstr[0] ""
62 | msgstr[1] ""
63 |
64 | msgid "should have at least %{count} item(s)"
65 | msgid_plural "should have at least %{count} item(s)"
66 | msgstr[0] ""
67 | msgstr[1] ""
68 |
69 | msgid "should be at most %{count} character(s)"
70 | msgid_plural "should be at most %{count} character(s)"
71 | msgstr[0] ""
72 | msgstr[1] ""
73 |
74 | msgid "should have at most %{count} item(s)"
75 | msgid_plural "should have at most %{count} item(s)"
76 | msgstr[0] ""
77 | msgstr[1] ""
78 |
79 | ## From Ecto.Changeset.validate_number/3
80 | msgid "must be less than %{number}"
81 | msgstr ""
82 |
83 | msgid "must be greater than %{number}"
84 | msgstr ""
85 |
86 | msgid "must be less than or equal to %{number}"
87 | msgstr ""
88 |
89 | msgid "must be greater than or equal to %{number}"
90 | msgstr ""
91 |
92 | msgid "must be equal to %{number}"
93 | msgstr ""
94 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here as no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 | ## From Ecto.Changeset.cast/4
12 | msgid "can't be blank"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.unique_constraint/3
16 | msgid "has already been taken"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.put_change/3
20 | msgid "is invalid"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_format/3
24 | msgid "has invalid format"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_subset/3
28 | msgid "has an invalid entry"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_exclusion/3
32 | msgid "is reserved"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_confirmation/3
36 | msgid "does not match confirmation"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.no_assoc_constraint/3
40 | msgid "is still associated to this entry"
41 | msgstr ""
42 |
43 | msgid "are still associated to this entry"
44 | msgstr ""
45 |
46 | ## From Ecto.Changeset.validate_length/3
47 | msgid "should be %{count} character(s)"
48 | msgid_plural "should be %{count} character(s)"
49 | msgstr[0] ""
50 | msgstr[1] ""
51 |
52 | msgid "should have %{count} item(s)"
53 | msgid_plural "should have %{count} item(s)"
54 | msgstr[0] ""
55 | msgstr[1] ""
56 |
57 | msgid "should be at least %{count} character(s)"
58 | msgid_plural "should be at least %{count} character(s)"
59 | msgstr[0] ""
60 | msgstr[1] ""
61 |
62 | msgid "should have at least %{count} item(s)"
63 | msgid_plural "should have at least %{count} item(s)"
64 | msgstr[0] ""
65 | msgstr[1] ""
66 |
67 | msgid "should be at most %{count} character(s)"
68 | msgid_plural "should be at most %{count} character(s)"
69 | msgstr[0] ""
70 | msgstr[1] ""
71 |
72 | msgid "should have at most %{count} item(s)"
73 | msgid_plural "should have at most %{count} item(s)"
74 | msgstr[0] ""
75 | msgstr[1] ""
76 |
77 | ## From Ecto.Changeset.validate_number/3
78 | msgid "must be less than %{number}"
79 | msgstr ""
80 |
81 | msgid "must be greater than %{number}"
82 | msgstr ""
83 |
84 | msgid "must be less than or equal to %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than or equal to %{number}"
88 | msgstr ""
89 |
90 | msgid "must be equal to %{number}"
91 | msgstr ""
92 |
--------------------------------------------------------------------------------
/zero-phoenix/test/controllers/person_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZeroPhoenix.PersonControllerTest do
2 | use ZeroPhoenix.ConnCase
3 |
4 | alias ZeroPhoenix.Person
5 | @valid_attrs %{"email": "some content", first_name: "some content", last_name: "some content", username: "some content"}
6 | @invalid_attrs %{}
7 |
8 | setup %{conn: conn} do
9 | {:ok, conn: put_req_header(conn, "accept", "application/json")}
10 | end
11 |
12 | test "lists all entries on index", %{conn: conn} do
13 | conn = get conn, person_path(conn, :index)
14 | assert json_response(conn, 200)["data"] == []
15 | end
16 |
17 | test "shows chosen resource", %{conn: conn} do
18 | person = Repo.insert! %Person{}
19 | conn = get conn, person_path(conn, :show, person)
20 | assert json_response(conn, 200)["data"] == %{"id" => person.id,
21 | "first_name" => person.first_name,
22 | "last_name" => person.last_name,
23 | "username" => person.username,
24 | "email" => person.email}
25 | end
26 |
27 | test "renders page not found when id is nonexistent", %{conn: conn} do
28 | assert_error_sent 404, fn ->
29 | get conn, person_path(conn, :show, -1)
30 | end
31 | end
32 |
33 | test "creates and renders resource when data is valid", %{conn: conn} do
34 | conn = post conn, person_path(conn, :create), person: @valid_attrs
35 | assert json_response(conn, 201)["data"]["id"]
36 | assert Repo.get_by(Person, @valid_attrs)
37 | end
38 |
39 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do
40 | conn = post conn, person_path(conn, :create), person: @invalid_attrs
41 | assert json_response(conn, 422)["errors"] != %{}
42 | end
43 |
44 | test "updates and renders chosen resource when data is valid", %{conn: conn} do
45 | person = Repo.insert! %Person{}
46 | conn = put conn, person_path(conn, :update, person), person: @valid_attrs
47 | assert json_response(conn, 200)["data"]["id"]
48 | assert Repo.get_by(Person, @valid_attrs)
49 | end
50 |
51 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do
52 | person = Repo.insert! %Person{}
53 | conn = put conn, person_path(conn, :update, person), person: @invalid_attrs
54 | assert json_response(conn, 422)["errors"] != %{}
55 | end
56 |
57 | test "deletes chosen resource", %{conn: conn} do
58 | person = Repo.insert! %Person{}
59 | conn = delete conn, person_path(conn, :delete, person)
60 | assert response(conn, 204)
61 | refute Repo.get(Person, person.id)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/zero-node/schema.js:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch';
2 | import {
3 | GraphQLID,
4 | GraphQLList,
5 | GraphQLNonNull,
6 | GraphQLObjectType,
7 | GraphQLSchema,
8 | GraphQLString,
9 | } from 'graphql';
10 | import {
11 | fromGlobalId,
12 | globalIdField,
13 | nodeDefinitions,
14 | } from 'graphql-relay';
15 |
16 | const {
17 | nodeField,
18 | nodeInterface,
19 | } = nodeDefinitions(
20 | // A method that maps from a global id to an object
21 | (globalId, {loaders}) => {
22 | const {id, type} = fromGlobalId(globalId);
23 | if (type === 'Person') {
24 | return loaders.person.load(id);
25 | }
26 | },
27 | // A method that maps from an object to a type
28 | (obj) => {
29 | if (obj.hasOwnProperty('username')) {
30 | return PersonType;
31 | }
32 | }
33 | );
34 |
35 | const PersonType = new GraphQLObjectType({
36 | name: 'Person',
37 | description: 'Somebody that you used to know',
38 | fields: () => ({
39 | id: globalIdField('Person'),
40 | firstName: {
41 | type: GraphQLString,
42 | description: 'What you yell at me',
43 | resolve: obj => obj.first_name,
44 | },
45 | lastName: {
46 | type: GraphQLString,
47 | description: 'What you yell at me when I\'ve been bad',
48 | resolve: obj => obj.last_name,
49 | },
50 | fullName: {
51 | type: GraphQLString,
52 | description: 'A name sandwich',
53 | resolve: obj => `${obj.first_name} ${obj.last_name}`,
54 | },
55 | email: {
56 | type: GraphQLString,
57 | description: 'Where to send junk mail',
58 | },
59 | username: {
60 | type: GraphQLString,
61 | description: 'Log in as this',
62 | },
63 | friends: {
64 | type: new GraphQLList(PersonType),
65 | description: 'People who lent you money',
66 | resolve: (obj, args, {loaders}) =>
67 | loaders.person.loadManyByURL(obj.friends),
68 | },
69 | }),
70 | interfaces: [nodeInterface],
71 | });
72 |
73 | const QueryType = new GraphQLObjectType({
74 | name: 'Query',
75 | description: 'The root of all... queries',
76 | fields: () => ({
77 | allPeople: {
78 | type: new GraphQLList(PersonType),
79 | description: 'Everyone, everywhere',
80 | resolve: (root, args, {loaders}) => loaders.person.loadAll(),
81 | },
82 | node: nodeField,
83 | person: {
84 | type: PersonType,
85 | args: {
86 | id: {type: new GraphQLNonNull(GraphQLID)},
87 | },
88 | resolve: (root, args, {loaders}) => loaders.person.load(args.id),
89 | },
90 | }),
91 | });
92 |
93 | export default new GraphQLSchema({
94 | query: QueryType,
95 | });
96 |
--------------------------------------------------------------------------------
/zero-scala/src/main/scala/Server.scala:
--------------------------------------------------------------------------------
1 | import SchemaDefinition.personFetcher
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.server.Directives._
6 | import akka.http.scaladsl.model.StatusCodes._
7 | import akka.http.scaladsl.server._
8 | import akka.stream.ActorMaterializer
9 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
10 |
11 | import sangria.parser.QueryParser
12 | import sangria.execution._
13 | import sangria.execution.deferred.DeferredResolver
14 | import sangria.marshalling.sprayJson._
15 | import sangria.schema.Schema
16 |
17 | import spray.json._
18 |
19 | import scala.util.{Failure, Success}
20 |
21 | object Server extends App {
22 | implicit val system = ActorSystem("sangria-server")
23 | implicit val materializer = ActorMaterializer()
24 |
25 | import system.dispatcher
26 |
27 | val repository = Repository.createDatabase()
28 |
29 | case object TooComplexQuery extends Exception
30 |
31 | val rejectComplexQueries = QueryReducer.rejectComplexQueries(300,
32 | (_: Double, _: Repository) ⇒ TooComplexQuery)
33 |
34 | val exceptionHandler: Executor.ExceptionHandler = {
35 | case (_, TooComplexQuery) ⇒ HandledException("Too complex query. Please reduce the field selection.")
36 | }
37 |
38 | def executeGraphQLQuery(schema: Schema[Repository, Unit], queryInfo: JsValue) = {
39 | val JsObject(fields) = queryInfo
40 | val JsString(query) = fields("query")
41 | val operation = fields.get("operationName") collect {
42 | case JsString(op) ⇒ op
43 | }
44 |
45 | val vars = fields.get("variables") match {
46 | case Some(obj: JsObject) ⇒ obj
47 | case Some(JsString(s)) if s.trim.nonEmpty ⇒ s.parseJson
48 | case _ ⇒ JsObject.empty
49 | }
50 |
51 | QueryParser.parse(query) match {
52 |
53 | // query parsed successfully, time to execute it!
54 | case Success(queryDocument) ⇒
55 | complete(Executor.execute(schema, queryDocument, repository,
56 | variables = vars,
57 | operationName = operation,
58 | queryReducers = rejectComplexQueries :: Nil,
59 | exceptionHandler = exceptionHandler,
60 | deferredResolver = DeferredResolver.fetchers(personFetcher))
61 | .map(OK → _)
62 | .recover {
63 | case error: QueryAnalysisError ⇒ BadRequest → error.resolveError
64 | case error: ErrorWithResolver ⇒ InternalServerError → error.resolveError
65 | })
66 |
67 | // can't parse GraphQL query, return error
68 | case Failure(error) ⇒
69 | complete(BadRequest, JsObject("error" → JsString(error.getMessage)))
70 | }
71 | }
72 |
73 | val route: Route =
74 | (post & path("graphql")) {
75 | entity(as[JsValue]) { requestJson ⇒
76 | executeGraphQLQuery(SchemaDefinition.schema, requestJson)
77 | }
78 | } ~
79 | get {
80 | getFromResource("graphiql.html")
81 | }
82 |
83 | Http().bindAndHandle(route, "0.0.0.0", 8080)
84 | }
85 |
--------------------------------------------------------------------------------
/zero-django/people/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9.5 on 2016-04-12 03:17
3 | from __future__ import unicode_literals
4 |
5 | import django.contrib.auth.models
6 | import django.core.validators
7 | from django.db import migrations, models
8 | import django.utils.timezone
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | initial = True
14 |
15 | dependencies = [
16 | ('auth', '0007_alter_validators_add_error_messages'),
17 | ]
18 |
19 | operations = [
20 | migrations.CreateModel(
21 | name='Person',
22 | fields=[
23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24 | ('password', models.CharField(max_length=128, verbose_name='password')),
25 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
26 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
27 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username')),
28 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
29 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
30 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
31 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
32 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
33 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
34 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
35 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
36 | ],
37 | options={
38 | 'db_table': 'person',
39 | },
40 | managers=[
41 | ('objects', django.contrib.auth.models.UserManager()),
42 | ],
43 | ),
44 | ]
45 |
--------------------------------------------------------------------------------
/zero-scala/src/main/scala/Repository.scala:
--------------------------------------------------------------------------------
1 | import language.postfixOps
2 | import scala.concurrent.Await
3 | import scala.concurrent.duration._
4 | import slick.jdbc.H2Profile.api._
5 |
6 | import scala.concurrent.ExecutionContext.Implicits.global
7 |
8 | case class Person(id: String, firstName: String, lastName: String, username: String, email: String)
9 |
10 | case class Friend(personId: String, friendId: String)
11 |
12 | class Repository(db: Database) {
13 | import Repository._
14 |
15 | def allPeople =
16 | db.run(People.result)
17 |
18 | def people(ids: Seq[String]) =
19 | db.run(People.filter(_.id inSet ids).result)
20 |
21 | def person(id: String) =
22 | db.run(People.filter(_.id === id).result.headOption)
23 |
24 | def findFriends(personIds: Seq[String]) =
25 | db.run(friendsQuery(personIds).result).map(result ⇒
26 | result.groupBy(_._2.id).toVector.map {
27 | case (_, friends) ⇒ friends.map(_._1.personId) → friends.head._2
28 | })
29 |
30 | def close() = db.close()
31 | }
32 |
33 | object Repository {
34 | class PersonTable(tag: Tag) extends Table[Person](tag, "PEOPLE") {
35 | def id = column[String]("PERSON_ID", O.PrimaryKey)
36 | def firstName = column[String]("FIRST_NAME")
37 | def lastName = column[String]("LAST_NAME")
38 | def username = column[String]("USERNAME")
39 | def email = column[String]("EMAIL")
40 |
41 | def * = (id, firstName, lastName, username, email) <> ((Person.apply _).tupled, Person.unapply)
42 | }
43 |
44 | val People = TableQuery[PersonTable]
45 |
46 | class FriendTable(tag: Tag) extends Table[Friend](tag, "FRIENDS") {
47 | def personId = column[String]("PERSON_ID")
48 | def friendId = column[String]("FRIEND_ID")
49 |
50 | def person = foreignKey("PERSON_FK", personId, People)(_.id)
51 | def friend = foreignKey("FRIEND_FK", friendId, People)(_.id)
52 | def idx = index("UNIQUE_IDX", (personId, friendId), unique = true)
53 |
54 | def * = (personId, friendId) <> ((Friend.apply _).tupled, Friend.unapply)
55 | }
56 |
57 | val Friends = TableQuery[FriendTable]
58 |
59 | val InitialDatabaseSetup = DBIO.seq(
60 | (People.schema ++ Friends.schema).create,
61 |
62 | People ++= Seq(
63 | Person("1000", "Brianna", "Stephenson", "brianna.stephenson", "justo.eu@Lorem.edu"),
64 | Person("1001", "Leslie", "Vasquez", "leslie.vasquez", "eu@Duiscursusdiam.co.uk"),
65 | Person("1002", "Garrison", "Douglas", "garrison.douglas", "rutrum@tristiquesenectuset.com"),
66 | Person("1003", "Jena", "Brady", "jena.brady", "blandit.Nam@Inat.ca"),
67 | Person("1004", "Evan", "Cain", "evan.cain", "sem.ut.dolor@etarcu.co.uk"),
68 | Person("1005", "Alexandra", "Evans", "alexandra.evans", "nisi.Mauris@Fuscealiquet.co.uk"),
69 | Person("1006", "Nigel", "May", "nigel.may", "semper.et@metussitamet.ca")),
70 |
71 | Friends ++= Seq(
72 | Friend("1000", "1001"),
73 | Friend("1000", "1004"),
74 | Friend("1000", "1006"),
75 | Friend("1001", "1003"),
76 | Friend("1001", "1005"),
77 | Friend("1002", "1004"),
78 | Friend("1003", "1005"),
79 | Friend("1003", "1006"),
80 | Friend("1005", "1001"),
81 | Friend("1005", "1004")))
82 |
83 | def createDatabase() = {
84 | val db = Database.forConfig("memoryDb")
85 |
86 | Await.result(db.run(InitialDatabaseSetup), 10 seconds)
87 |
88 | new Repository(db)
89 | }
90 |
91 | private def friendsQuery(personIds: Seq[String]) =
92 | Friends.filter(_.personId inSet personIds)
93 | .join(People).on(_.friendId === _.id)
94 | }
95 |
--------------------------------------------------------------------------------
/zero-rails/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Disable serving static files from the `/public` folder by default since
18 | # Apache or NGINX already handles this.
19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
20 |
21 | # Compress JavaScripts and CSS.
22 | config.assets.js_compressor = :uglifier
23 | # config.assets.css_compressor = :sass
24 |
25 | # Do not fallback to assets pipeline if a precompiled asset is missed.
26 | config.assets.compile = false
27 |
28 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
29 | # yet still be able to expire them through the digest params.
30 | config.assets.digest = true
31 |
32 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
33 |
34 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
35 | # config.action_controller.asset_host = 'http://assets.example.com'
36 |
37 | # Specifies the header that your server uses for sending files.
38 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
40 |
41 | # Action Cable endpoint configuration
42 | # config.action_cable.url = 'wss://example.com/cable'
43 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
44 |
45 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
46 | # config.force_ssl = true
47 |
48 | # Use the lowest log level to ensure availability of diagnostic information
49 | # when problems arise.
50 | config.log_level = :debug
51 |
52 | # Prepend all log lines with the following tags.
53 | config.log_tags = [ :request_id ]
54 |
55 | # Use a different logger for distributed setups.
56 | # require 'syslog/logger'
57 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
58 |
59 | if ENV["RAILS_LOG_TO_STDOUT"].present?
60 | config.logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
61 | end
62 |
63 | # Use a different cache store in production.
64 | # config.cache_store = :mem_cache_store
65 |
66 | # Use a real queuing backend for Active Job (and separate queues per environment)
67 | # config.active_job.queue_adapter = :resque
68 | # config.active_job.queue_name_prefix = "zero-rails_#{Rails.env}"
69 | config.action_mailer.perform_caching = false
70 |
71 | # Ignore bad email addresses and do not raise email delivery errors.
72 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
73 | # config.action_mailer.raise_delivery_errors = false
74 |
75 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
76 | # the I18n.default_locale when a translation cannot be found).
77 | config.i18n.fallbacks = true
78 |
79 | # Send deprecation notices to registered listeners.
80 | config.active_support.deprecation = :notify
81 |
82 | # Use default logging formatter so that PID and timestamp are not suppressed.
83 | config.log_formatter = ::Logger::Formatter.new
84 |
85 | # Do not dump schema after migrations.
86 | config.active_record.dump_schema_after_migration = false
87 | end
88 |
--------------------------------------------------------------------------------
/zero-django/zero_django/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for zero_django project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.9.5.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.9/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.9/ref/settings/
11 | """
12 |
13 | from django.utils.crypto import get_random_string
14 |
15 | import os
16 |
17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19 |
20 |
21 | # Quick-start development settings - unsuitable for production
22 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
23 |
24 | # SECURITY WARNING: keep the secret key used in production secret!
25 | SECRET_KEY = get_random_string(50, 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)')
26 |
27 | # SECURITY WARNING: don't run with debug turned on in production!
28 | DEBUG = True
29 |
30 | ALLOWED_HOSTS = []
31 |
32 |
33 | # Application definition
34 |
35 | INSTALLED_APPS = [
36 | 'django.contrib.admin',
37 | 'django.contrib.auth',
38 | 'django.contrib.contenttypes',
39 | 'django.contrib.sessions',
40 | 'django.contrib.messages',
41 | 'django.contrib.staticfiles',
42 | 'people',
43 | 'django_graphiql',
44 | 'graphene_django',
45 | ]
46 |
47 | MIDDLEWARE_CLASSES = [
48 | 'django.middleware.security.SecurityMiddleware',
49 | 'django.contrib.sessions.middleware.SessionMiddleware',
50 | 'django.middleware.common.CommonMiddleware',
51 | 'django.middleware.csrf.CsrfViewMiddleware',
52 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
53 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
54 | 'django.contrib.messages.middleware.MessageMiddleware',
55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
56 | ]
57 |
58 | ROOT_URLCONF = 'zero_django.urls'
59 |
60 | TEMPLATES = [
61 | {
62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
63 | 'DIRS': [],
64 | 'APP_DIRS': True,
65 | 'OPTIONS': {
66 | 'context_processors': [
67 | 'django.template.context_processors.debug',
68 | 'django.template.context_processors.request',
69 | 'django.contrib.auth.context_processors.auth',
70 | 'django.contrib.messages.context_processors.messages',
71 | ],
72 | },
73 | },
74 | ]
75 |
76 | WSGI_APPLICATION = 'zero_django.wsgi.application'
77 |
78 |
79 | # Database
80 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases
81 |
82 | DATABASES = {
83 | 'default': {
84 | 'ENGINE': 'django.db.backends.sqlite3',
85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
86 | }
87 | }
88 |
89 |
90 | # Password validation
91 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
92 |
93 | AUTH_PASSWORD_VALIDATORS = [
94 | {
95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
96 | },
97 | {
98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
99 | },
100 | {
101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
102 | },
103 | {
104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
105 | },
106 | ]
107 |
108 |
109 | # Internationalization
110 | # https://docs.djangoproject.com/en/1.9/topics/i18n/
111 |
112 | LANGUAGE_CODE = 'en-us'
113 |
114 | TIME_ZONE = 'UTC'
115 |
116 | USE_I18N = True
117 |
118 | USE_L10N = True
119 |
120 | USE_TZ = True
121 |
122 |
123 | # Static files (CSS, JavaScript, Images)
124 | # https://docs.djangoproject.com/en/1.9/howto/static-files/
125 |
126 | STATIC_URL = '/static/'
127 |
128 | # Override the default auth user model to something more personable
129 | AUTH_USER_MODEL = 'people.Person'
130 |
--------------------------------------------------------------------------------
/zero-rails/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (5.0.0.beta3)
5 | actionpack (= 5.0.0.beta3)
6 | nio4r (~> 1.2)
7 | websocket-driver (~> 0.6.1)
8 | actionmailer (5.0.0.beta3)
9 | actionpack (= 5.0.0.beta3)
10 | actionview (= 5.0.0.beta3)
11 | activejob (= 5.0.0.beta3)
12 | mail (~> 2.5, >= 2.5.4)
13 | rails-dom-testing (~> 1.0, >= 1.0.5)
14 | actionpack (5.0.0.beta3)
15 | actionview (= 5.0.0.beta3)
16 | activesupport (= 5.0.0.beta3)
17 | rack (~> 2.x)
18 | rack-test (~> 0.6.3)
19 | rails-dom-testing (~> 1.0, >= 1.0.5)
20 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
21 | actionview (5.0.0.beta3)
22 | activesupport (= 5.0.0.beta3)
23 | builder (~> 3.1)
24 | erubis (~> 2.7.0)
25 | rails-dom-testing (~> 1.0, >= 1.0.5)
26 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
27 | active_model_serializers (0.9.5)
28 | activemodel (>= 3.2)
29 | activejob (5.0.0.beta3)
30 | activesupport (= 5.0.0.beta3)
31 | globalid (>= 0.3.6)
32 | activemodel (5.0.0.beta3)
33 | activesupport (= 5.0.0.beta3)
34 | activerecord (5.0.0.beta3)
35 | activemodel (= 5.0.0.beta3)
36 | activesupport (= 5.0.0.beta3)
37 | arel (~> 7.0)
38 | activesupport (5.0.0.beta3)
39 | concurrent-ruby (~> 1.0)
40 | i18n (~> 0.7)
41 | minitest (~> 5.1)
42 | tzinfo (~> 1.1)
43 | arel (7.0.0)
44 | blankslate (3.1.3)
45 | builder (3.2.2)
46 | byebug (8.2.4)
47 | coffee-rails (4.1.1)
48 | coffee-script (>= 2.2.0)
49 | railties (>= 4.0.0, < 5.1.x)
50 | coffee-script (2.4.1)
51 | coffee-script-source
52 | execjs
53 | coffee-script-source (1.10.0)
54 | concurrent-ruby (1.0.1)
55 | debug_inspector (0.0.2)
56 | erubis (2.7.0)
57 | execjs (2.6.0)
58 | ffi (1.9.10)
59 | globalid (0.3.6)
60 | activesupport (>= 4.1.0)
61 | graphiql-rails (1.1.0)
62 | rails
63 | graphql (0.12.0)
64 | parslet (~> 1.6)
65 | i18n (0.7.0)
66 | jbuilder (2.4.1)
67 | activesupport (>= 3.0.0, < 5.1)
68 | multi_json (~> 1.2)
69 | jquery-rails (4.1.1)
70 | rails-dom-testing (>= 1, < 3)
71 | railties (>= 4.2.0)
72 | thor (>= 0.14, < 2.0)
73 | json (1.8.3)
74 | listen (3.0.6)
75 | rb-fsevent (>= 0.9.3)
76 | rb-inotify (>= 0.9.7)
77 | loofah (2.0.3)
78 | nokogiri (>= 1.5.9)
79 | mail (2.6.4)
80 | mime-types (>= 1.16, < 4)
81 | method_source (0.8.2)
82 | mime-types (3.0)
83 | mime-types-data (~> 3.2015)
84 | mime-types-data (3.2016.0221)
85 | mini_portile2 (2.0.0)
86 | minitest (5.8.4)
87 | multi_json (1.11.2)
88 | nio4r (1.2.1)
89 | nokogiri (1.6.7.2)
90 | mini_portile2 (~> 2.0.0.rc2)
91 | parslet (1.7.1)
92 | blankslate (>= 2.0, <= 4.0)
93 | puma (3.4.0)
94 | rack (2.0.0.alpha)
95 | json
96 | rack-test (0.6.3)
97 | rack (>= 1.0)
98 | rails (5.0.0.beta3)
99 | actioncable (= 5.0.0.beta3)
100 | actionmailer (= 5.0.0.beta3)
101 | actionpack (= 5.0.0.beta3)
102 | actionview (= 5.0.0.beta3)
103 | activejob (= 5.0.0.beta3)
104 | activemodel (= 5.0.0.beta3)
105 | activerecord (= 5.0.0.beta3)
106 | activesupport (= 5.0.0.beta3)
107 | bundler (>= 1.3.0, < 2.0)
108 | railties (= 5.0.0.beta3)
109 | sprockets-rails (>= 2.0.0)
110 | rails-deprecated_sanitizer (1.0.3)
111 | activesupport (>= 4.2.0.alpha)
112 | rails-dom-testing (1.0.7)
113 | activesupport (>= 4.2.0.beta, < 5.0)
114 | nokogiri (~> 1.6.0)
115 | rails-deprecated_sanitizer (>= 1.0.1)
116 | rails-html-sanitizer (1.0.3)
117 | loofah (~> 2.0)
118 | railties (5.0.0.beta3)
119 | actionpack (= 5.0.0.beta3)
120 | activesupport (= 5.0.0.beta3)
121 | method_source
122 | rake (>= 0.8.7)
123 | thor (>= 0.18.1, < 2.0)
124 | rake (11.1.2)
125 | rb-fsevent (0.9.7)
126 | rb-inotify (0.9.7)
127 | ffi (>= 0.5.0)
128 | sass (3.4.22)
129 | sass-rails (5.0.4)
130 | railties (>= 4.0.0, < 5.0)
131 | sass (~> 3.1)
132 | sprockets (>= 2.8, < 4.0)
133 | sprockets-rails (>= 2.0, < 4.0)
134 | tilt (>= 1.1, < 3)
135 | spring (1.7.0)
136 | spring-watcher-listen (2.0.0)
137 | listen (>= 2.7, < 4.0)
138 | spring (~> 1.2)
139 | sprockets (3.6.0)
140 | concurrent-ruby (~> 1.0)
141 | rack (> 1, < 3)
142 | sprockets-rails (3.0.4)
143 | actionpack (>= 4.0)
144 | activesupport (>= 4.0)
145 | sprockets (>= 3.0.0)
146 | sqlite3 (1.3.11)
147 | thor (0.19.1)
148 | thread_safe (0.3.5)
149 | tilt (2.0.2)
150 | turbolinks (5.0.0.beta2)
151 | turbolinks-source
152 | turbolinks-source (5.0.0.beta4)
153 | tzinfo (1.2.2)
154 | thread_safe (~> 0.1)
155 | uglifier (3.0.0)
156 | execjs (>= 0.3.0, < 3)
157 | web-console (3.1.1)
158 | activemodel (>= 4.2)
159 | debug_inspector
160 | railties (>= 4.2)
161 | websocket-driver (0.6.3)
162 | websocket-extensions (>= 0.1.0)
163 | websocket-extensions (0.1.2)
164 |
165 | PLATFORMS
166 | ruby
167 |
168 | DEPENDENCIES
169 | active_model_serializers
170 | byebug
171 | coffee-rails (~> 4.1.0)
172 | graphiql-rails
173 | graphql
174 | jbuilder (~> 2.0)
175 | jquery-rails
176 | listen (~> 3.0.5)
177 | puma
178 | rails (>= 5.0.0.beta3, < 5.1)
179 | sass-rails (~> 5.0)
180 | spring
181 | spring-watcher-listen (~> 2.0.0)
182 | sqlite3
183 | turbolinks (~> 5.x)
184 | tzinfo-data
185 | uglifier (>= 1.3.0)
186 | web-console (~> 3.0)
187 |
188 | BUNDLED WITH
189 | 1.11.2
190 |
--------------------------------------------------------------------------------
/zero-scala/src/main/resources/graphiql.html:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Loading...
53 |
54 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/zero-phoenix/mix.lock:
--------------------------------------------------------------------------------
1 | %{"absinthe": {:hex, :absinthe, "1.3.0", "0b58aec87c115025c6abbbdaebdd2b5d545d5c47a342e5a8c790d5989d27b24c", [:mix], []},
2 | "absinthe_plug": {:hex, :absinthe_plug, "1.3.0", "52bcf04fc95463843cbd78994f7dd9b587b299c7065081582d37d59cdcc68d98", [:mix], [{:absinthe, "~> 1.3.0", [hex: :absinthe, optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, optional: false]}]},
3 | "certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []},
4 | "combine": {:hex, :combine, "0.9.1", "5fd778ee77032ae593bf79aedb8519d9e36283e4f869abd98c2d6029ca476db8", [:mix], []},
5 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []},
6 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]},
7 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
8 | "db_connection": {:hex, :db_connection, "1.0.0-rc.4", "fad1f772c151cc6bde82412b8d72319968bc7221df8ef7d5e9d7fde7cb5c86b7", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0.0-beta.3", [hex: :sbroker, optional: true]}]},
9 | "decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []},
10 | "ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "16529476ecaeb4af92ceeea2aa41094426910b4c", []},
11 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
12 | "gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []},
13 | "graphql": {:hex, :graphql, "0.3.1", "d3bb5467877456cc2b33debc75407e9216567b10e35e83d5195e2d51e835e8c7", [:mix], []},
14 | "hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]},
15 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []},
16 | "inflex": {:hex, :inflex, "1.5.0", "e4ff5d900280b2011b24d1ac1c4590986ee5add2ea644c9894e72213cf93ff0b", [:mix], []},
17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
18 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []},
19 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
20 | "moebius": {:hex, :moebius, "2.0.3", "5cd3c60c685876253edff169f55aab600cb6b73d50166364f13b90a904409998", [:mix], [{:inflex, "~> 1.5.0", [hex: :inflex, optional: false]}, {:poison, "~> 2.0.1", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.11.0", [hex: :postgrex, optional: false]}, {:timex, "~> 2.0", [hex: :timex, optional: false]}]},
21 | "phoenix": {:hex, :phoenix, "1.2.0", "1bdeb99c254f4c534cdf98fd201dede682297ccc62fcac5d57a2627c3b6681fb", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]},
22 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.0.0", "b947aaf03d076f5b1448f87828f22fb7710478ee38455c67cc3fe8e9a4dfd015", [:mix], [{:ecto, "~> 2.0.0-rc", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}]},
23 | "phoenix_html": {:hex, :phoenix_html, "2.6.2", "944a5e581b0d899e4f4c838a69503ebd05300fe35ba228a74439e6253e10e0c0", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]},
24 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.5", "829218c4152ba1e9848e2bf8e161fcde6b4ec679a516259442561d21fde68d0b", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]},
25 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.0", "c31af4be22afeeebfaf246592778c8c840e5a1ddc7ca87610c41ccfb160c2c57", [:mix], []},
26 | "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
27 | "plug_graphql": {:hex, :plug_graphql, "0.3.1", "8c3c91b889236620cea19606ab5b770c1c533242c92d97c23b9f359be4eb4c03", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: false]}, {:graphql, "~> 0.3", [hex: :graphql, optional: false]}, {:plug, "~> 0.14 or ~> 1.0", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]},
28 | "poison": {:hex, :poison, "2.0.1", "81248a36d1b602b17ea6556bfa8952492091f01af05173de11f8b297e2bbf088", [:mix], []},
29 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
30 | "postgrex": {:hex, :postgrex, "0.11.2", "139755c1359d3c5c6d6e8b1ea72556d39e2746f61c6ddfb442813c91f53487e8", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]},
31 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []},
32 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []},
33 | "timex": {:hex, :timex, "2.2.1", "0d69012a7fd69f4cbdaa00cc5f2a5f30f1bed56072fb362ed4bddf60db343022", [:mix], [{:combine, "~> 0.7", [hex: :combine, optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]},
34 | "tzdata": {:hex, :tzdata, "0.5.8", "a4ffe564783c6519e4df230a5d0e1cf44b7db7f576bcae76d05540b5da5b6143", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, optional: false]}]}}
35 |
--------------------------------------------------------------------------------
/zero-phoenix/README.md:
--------------------------------------------------------------------------------
1 | # Phoenix Example
2 |
3 | The purpose of this example is to provide details as to how one would go about using GraphQL with the Phoenix Web Framework. Thus, I have created two major sections which should be self explanatory: Quick Installation and Tutorial Installation.
4 |
5 | ## Getting Started
6 |
7 | ## Software requirements
8 |
9 | - [Elixir 1.4.0 or higher](http://elixir-lang.org/install.html)
10 |
11 | - [Phoenix 1.2.0 or higher](http://www.phoenixframework.org/docs/installation)
12 |
13 | - PostgreSQL 9.6.x or higher
14 |
15 | ## Communication
16 |
17 | - If you **need help**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/graphql). (Tag 'graphql')
18 | - If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/graphql).
19 | - If you **found a bug**, open an issue.
20 | - If you **have a feature request**, open an issue.
21 | - If you **want to contribute**, submit a pull request.
22 |
23 | ## Quick Installation
24 |
25 | 1. clone this repository
26 |
27 | ```
28 | $ git clone git@github.com:steveluscher/zero-to-graphql.git
29 | ```
30 |
31 | 2. change directory location
32 |
33 | ```
34 | $ cd /path/to/zero-phoenix
35 | ```
36 |
37 | 2. install dependencies
38 |
39 | ```
40 | $ mix deps.get
41 | ```
42 |
43 | 3. create, migrate, and seed the database
44 |
45 | ```
46 | $ mix ecto.create
47 | $ mix ecto.migrate
48 | $ mix ecto.seed
49 | ```
50 |
51 | 4. start the server
52 |
53 | ```
54 | $ mix phoenix.server
55 | ```
56 |
57 | 5. navigate to our application within the browser
58 |
59 | ```
60 | $ open http://localhost:4000/graphiql
61 | ```
62 |
63 | 6. enter and run GraphQL query
64 |
65 | ```
66 | {
67 | person(id: "1") {
68 | firstName
69 | lastName
70 | username
71 | email
72 | friends {
73 | firstName
74 | lastName
75 | username
76 | email
77 | }
78 | }
79 | }
80 | ```
81 |
82 | Note: The GraphQL query is responding with same response but different shape
83 | within the GraphiQL browser because Elixir Maps perform no ordering on insertion.
84 |
85 | ## Tutorial Installation
86 |
87 | 1. create the project
88 |
89 | ```
90 | $ mix phoenix.new zero_phoenix --no-brunch
91 | ```
92 |
93 | Note: Just answer 'Y' to all the prompts that appear.
94 |
95 | 2. change the folder name to more consistent with the GraphQL folders
96 |
97 | ```
98 | $ mv zero_phoenix zero-phoenix
99 | ```
100 |
101 | 3. switch to the project directory
102 |
103 | ```
104 | $ cd zero-phoenix
105 | ```
106 |
107 | 4. update `username` and `password` database credentials which appears at the bottom of the following files:
108 |
109 | ```
110 | config/dev.exs
111 | config/test.exs
112 | ```
113 |
114 | 5. generate an API for representing our `Person` resource
115 |
116 | ```
117 | $ mix phoenix.gen.json Person people first_name:string last_name:string username:string email:string
118 | ```
119 |
120 | 6. replace the generated `Person` model with the following:
121 |
122 | `web/models/person.ex`:
123 |
124 | ```elixir
125 | defmodule ZeroPhoenix.Person do
126 | use ZeroPhoenix.Web, :model
127 |
128 | @required_fields ~w(first_name last_name username email)
129 | @optional_fields ~w()
130 |
131 | schema "people" do
132 | field :first_name, :string
133 | field :last_name, :string
134 | field :username, :string
135 | field :email, :string
136 |
137 | has_many :friendships, ZeroPhoenix.Friendship
138 | has_many :friends, through: [:friendships, :friend]
139 |
140 | timestamps()
141 | end
142 |
143 | @doc """
144 | Builds a changeset based on the `struct` and `params`.
145 | """
146 | def changeset(struct, params \\ %{}) do
147 | struct
148 | |> cast(params, @required_fields)
149 | |> validate_required(@required_fields)
150 | end
151 | end
152 | ```
153 |
154 | 7. add the resource to your api scope in which should look as follows after the edit:
155 |
156 | `web/router.ex`:
157 |
158 | ```elixir
159 | scope "/api", ZeroPhoenix do
160 | pipe_through :api
161 |
162 | resources "/people", PersonController, except: [:new, :edit]
163 | end
164 | ```
165 |
166 | Note: When creating an API, one doesn't require a new or edit actions. Thus, this is the reason that we are excluding them from this resource.
167 |
168 | 8. create and migrate the database
169 |
170 | ```
171 | $ mix ecto.create
172 | $ mix ecto.migrate
173 | ```
174 |
175 | 9. generate a `Friendship` model which representing our join model:
176 |
177 | ```
178 | $ mix phoenix.gen.model Friendship friendships person_id:references:people friend_id:references:people
179 | ```
180 |
181 | 10. replace the generated `Friendship` model with the following:
182 |
183 | `web/models/friendship.ex`:
184 |
185 | ```elixir
186 | defmodule ZeroPhoenix.Friendship do
187 | use ZeroPhoenix.Web, :model
188 |
189 | @required_fields ~w(person_id friend_id)
190 | @optional_fields ~w()
191 |
192 | schema "friendships" do
193 | belongs_to :person, ZeroPhoenix.Person
194 | belongs_to :friend, ZeroPhoenix.Person
195 |
196 | timestamps()
197 | end
198 |
199 | @doc """
200 | Builds a changeset based on the `struct` and `params`.
201 | """
202 | def changeset(struct, params \\ %{}) do
203 | struct
204 | |> cast(params, @required_fields)
205 | |> validate_required(@required_fields)
206 | end
207 | end
208 | ```
209 |
210 | Note: We want `friend_id` to reference the `people` table because our `friend_id` really represents a `Person` model.
211 |
212 | 11. migrate the database
213 |
214 | ```
215 | $ mix ecto.migrate
216 | ```
217 |
218 | 12. create the seeds file
219 |
220 | `priv/repo/seeds.exs`:
221 |
222 |
223 | ```
224 | alias ZeroPhoenix.Repo
225 | alias ZeroPhoenix.Person
226 | alias ZeroPhoenix.Friendship
227 |
228 | # reset the datastore
229 | Repo.delete_all(Person)
230 |
231 | # insert people
232 | me = Repo.insert!(%Person{ first_name: "Steven", last_name: "Luscher", email: "steveluscher@fb.com", username: "steveluscher" })
233 | dhh = Repo.insert!(%Person{ first_name: "David", last_name: "Heinemeier Hansson", email: "dhh@37signals.com", username: "dhh" })
234 | ezra = Repo.insert!(%Person{ first_name: "Ezra", last_name: "Zygmuntowicz", email: "ezra@merbivore.com", username: "ezra" })
235 | matz = Repo.insert!(%Person{ first_name: "Yukihiro", last_name: "Matsumoto", email: "matz@heroku.com", username: "matz" })
236 |
237 | me
238 | |> Ecto.build_assoc(:friendships)
239 | |> Friendship.changeset( %{ person_id: me.id, friend_id: matz.id } )
240 | |> Repo.insert
241 |
242 | dhh
243 | |> Ecto.build_assoc(:friendships)
244 | |> Friendship.changeset( %{ person_id: dhh.id, friend_id: ezra.id } )
245 | |> Repo.insert
246 |
247 | dhh
248 | |> Ecto.build_assoc(:friendships)
249 | |> Friendship.changeset( %{ person_id: dhh.id, friend_id: matz.id } )
250 | |> Repo.insert
251 |
252 | ezra
253 | |> Ecto.build_assoc(:friendships)
254 | |> Friendship.changeset( %{ person_id: ezra.id, friend_id: dhh.id } )
255 | |> Repo.insert
256 |
257 | ezra
258 | |> Ecto.build_assoc(:friendships)
259 | |> Friendship.changeset( %{ person_id: ezra.id, friend_id: matz.id } )
260 | |> Repo.insert
261 |
262 | matz
263 | |> Ecto.build_assoc(:friendships)
264 | |> Friendship.changeset( %{ person_id: matz.id, friend_id: me.id } )
265 | |> Repo.insert
266 |
267 | matz
268 | |> Ecto.build_assoc(:friendships)
269 | |> Friendship.changeset( %{ person_id: matz.id, friend_id: ezra.id } )
270 | |> Repo.insert
271 |
272 | matz
273 | |> Ecto.build_assoc(:friendships)
274 | |> Friendship.changeset( %{ person_id: matz.id, friend_id: dhh.id } )
275 | |> Repo.insert
276 | ```
277 |
278 | 13. seed the database
279 |
280 | ```
281 | $ mix run priv/repo/seeds.exs
282 | ```
283 |
284 | 14. add `absinthe_plug` package to your `mix.exs` dependencies as follows:
285 |
286 | ```elixir
287 | defp deps do
288 | [
289 | {:phoenix, "~> 1.2.0"},
290 | {:phoenix_pubsub, "~> 1.0"},
291 | {:ecto, github: "elixir-ecto/ecto", override: true},
292 | {:phoenix_ecto, "~> 3.0"},
293 | {:postgrex, ">= 0.0.0"},
294 | {:phoenix_html, "~> 2.6"},
295 | {:phoenix_live_reload, "~> 1.0", only: :dev},
296 | {:gettext, "~> 0.11"},
297 | {:cowboy, "~> 1.0"},
298 | {:absinthe_plug, "~> 1.3"}
299 | ]
300 | end
301 | ```
302 |
303 | 15. Add `absinthe_plug` application to your `mix.exs` application as follows:
304 |
305 | ```elixir
306 | def application do
307 | [mod: {ZeroPhoenix, []},
308 | applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, :phoenix_ecto, :postgrex, :absinthe_plug]]
309 | end
310 | ```
311 |
312 | 16. update our projects dependencies:
313 |
314 | ```
315 | $ mix deps.get
316 | ```
317 |
318 | 17. add the GraphQL schema which represents our entry point into our GraphQL structure:
319 |
320 | `web/graphql/schema.ex`:
321 |
322 | ```elixir
323 | defmodule ZeroPhoenix.Graphql.Schema do
324 | use Absinthe.Schema
325 |
326 | import_types ZeroPhoenix.Graphql.Types.Person
327 |
328 | alias ZeroPhoenix.Repo
329 |
330 | query do
331 | field :person, type: :person do
332 | arg :id, non_null(:id)
333 | resolve fn %{id: id}, _info ->
334 | case ZeroPhoenix.Person|> Repo.get(id) do
335 | nil -> {:error, "Person id #{id} not found"}
336 | person -> {:ok, person}
337 | end
338 | end
339 | end
340 | end
341 | end
342 | ```
343 |
344 | 18. add our Person type which will be performing queries against:
345 |
346 | `web/graphql/types/person.ex`:
347 |
348 | ```elixir
349 | defmodule ZeroPhoenix.Graphql.Types.Person do
350 | use Absinthe.Schema.Notation
351 |
352 | import Ecto
353 |
354 | alias ZeroPhoenix.Repo
355 |
356 | @desc "a person"
357 | object :person do
358 | @desc "unique identifier for the person"
359 | field :id, non_null(:string)
360 |
361 | @desc "first name of a person"
362 | field :first_name, non_null(:string)
363 |
364 | @desc "last name of a person"
365 | field :last_name, non_null(:string)
366 |
367 | @desc "username of a person"
368 | field :username, non_null(:string)
369 |
370 | @desc "email of a person"
371 | field :email, non_null(:string)
372 |
373 | @desc "a list of friends for our person"
374 | field :friends, list_of(:person) do
375 | resolve fn _, %{source: person} ->
376 | {:ok, Repo.all(assoc(person, :friends))}
377 | end
378 | end
379 | end
380 | end
381 | ```
382 |
383 | 19. add route for mounting the GraphiQL browser endpoint:
384 |
385 | ```
386 | scope "/graphiql" do
387 | pipe_through :api
388 |
389 | forward "/", Absinthe.Plug.GraphiQL, schema: ZeroPhoenix.Graphql.Schema, interface: :simple
390 | end
391 | ```
392 |
393 | 20. start the server
394 |
395 | ```
396 | $ mix phoenix.server
397 | ```
398 |
399 | 21. navigate to our application within the browser
400 |
401 | ```
402 | $ open http://localhost:4000/graphiql
403 | ```
404 |
405 | 22. enter and run GraphQL query
406 |
407 | ```
408 | {
409 | person(id: "1") {
410 | firstName
411 | lastName
412 | username
413 | email
414 | friends {
415 | firstName
416 | lastName
417 | username
418 | email
419 | }
420 | }
421 | }
422 | ```
423 |
424 | Note: The GraphQL query is responding with same response but different shape
425 | within the GraphiQL browser because Elixir Maps perform no ordering on insertion.
426 |
427 | ## Production Setup
428 |
429 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment).
430 |
431 | ## Phoenix References
432 |
433 | * Official website: http://www.phoenixframework.org/
434 | * Guides: http://phoenixframework.org/docs/overview
435 | * Docs: https://hexdocs.pm/phoenix
436 | * Mailing list: http://groups.google.com/group/phoenix-talk
437 | * Source: https://github.com/phoenixframework/phoenix
438 |
439 | ## GraphQL References
440 |
441 | * Official Website: http://graphql.org
442 | * Absinthe GraphQL Elixir: http://absinthe-graphql.org
443 |
444 | ## Support
445 |
446 | Bug reports and feature requests can be filed with the rest for the Phoenix project here:
447 |
448 | * [File Bug Reports and Features](https://github.com/steveluscher/zero-to-graphql/issues)
449 |
450 | ## License
451 |
452 | ZeroPhoenix is released under the [MIT license](https://mit-license.org).
453 |
454 | ## Copyright
455 |
456 | copyright:: (c) Copyright 2016 Conrad Taylor. All Rights Reserved.
457 |
--------------------------------------------------------------------------------
/zero-phoenix/priv/static/js/phoenix.js:
--------------------------------------------------------------------------------
1 | (function(exports){
2 | "use strict";
3 |
4 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
5 |
6 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
7 |
8 | Object.defineProperty(exports, "__esModule", {
9 | value: true
10 | });
11 |
12 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
13 |
14 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
15 |
16 | // Phoenix Channels JavaScript client
17 | //
18 | // ## Socket Connection
19 | //
20 | // A single connection is established to the server and
21 | // channels are multiplexed over the connection.
22 | // Connect to the server using the `Socket` class:
23 | //
24 | // let socket = new Socket("/ws", {params: {userToken: "123"}})
25 | // socket.connect()
26 | //
27 | // The `Socket` constructor takes the mount point of the socket,
28 | // the authentication params, as well as options that can be found in
29 | // the Socket docs, such as configuring the `LongPoll` transport, and
30 | // heartbeat.
31 | //
32 | // ## Channels
33 | //
34 | // Channels are isolated, concurrent processes on the server that
35 | // subscribe to topics and broker events between the client and server.
36 | // To join a channel, you must provide the topic, and channel params for
37 | // authorization. Here's an example chat room example where `"new_msg"`
38 | // events are listened for, messages are pushed to the server, and
39 | // the channel is joined with ok/error/timeout matches:
40 | //
41 | // let channel = socket.channel("room:123", {token: roomToken})
42 | // channel.on("new_msg", msg => console.log("Got message", msg) )
43 | // $input.onEnter( e => {
44 | // channel.push("new_msg", {body: e.target.val}, 10000)
45 | // .receive("ok", (msg) => console.log("created message", msg) )
46 | // .receive("error", (reasons) => console.log("create failed", reasons) )
47 | // .receive("timeout", () => console.log("Networking issue...") )
48 | // })
49 | // channel.join()
50 | // .receive("ok", ({messages}) => console.log("catching up", messages) )
51 | // .receive("error", ({reason}) => console.log("failed join", reason) )
52 | // .receive("timeout", () => console.log("Networking issue. Still waiting...") )
53 | //
54 | //
55 | // ## Joining
56 | //
57 | // Creating a channel with `socket.channel(topic, params)`, binds the params to
58 | // `channel.params`, which are sent up on `channel.join()`.
59 | // Subsequent rejoins will send up the modified params for
60 | // updating authorization params, or passing up last_message_id information.
61 | // Successful joins receive an "ok" status, while unsuccessful joins
62 | // receive "error".
63 | //
64 | // ## Duplicate Join Subscriptions
65 | //
66 | // While the client may join any number of topics on any number of channels,
67 | // the client may only hold a single subscription for each unique topic at any
68 | // given time. When attempting to create a duplicate subscription,
69 | // the server will close the existing channel, log a warning, and
70 | // spawn a new channel for the topic. The client will have their
71 | // `channel.onClose` callbacks fired for the existing channel, and the new
72 | // channel join will have its receive hooks processed as normal.
73 | //
74 | // ## Pushing Messages
75 | //
76 | // From the previous example, we can see that pushing messages to the server
77 | // can be done with `channel.push(eventName, payload)` and we can optionally
78 | // receive responses from the push. Additionally, we can use
79 | // `receive("timeout", callback)` to abort waiting for our other `receive` hooks
80 | // and take action after some period of waiting. The default timeout is 5000ms.
81 | //
82 | //
83 | // ## Socket Hooks
84 | //
85 | // Lifecycle events of the multiplexed connection can be hooked into via
86 | // `socket.onError()` and `socket.onClose()` events, ie:
87 | //
88 | // socket.onError( () => console.log("there was an error with the connection!") )
89 | // socket.onClose( () => console.log("the connection dropped") )
90 | //
91 | //
92 | // ## Channel Hooks
93 | //
94 | // For each joined channel, you can bind to `onError` and `onClose` events
95 | // to monitor the channel lifecycle, ie:
96 | //
97 | // channel.onError( () => console.log("there was an error!") )
98 | // channel.onClose( () => console.log("the channel has gone away gracefully") )
99 | //
100 | // ### onError hooks
101 | //
102 | // `onError` hooks are invoked if the socket connection drops, or the channel
103 | // crashes on the server. In either case, a channel rejoin is attempted
104 | // automatically in an exponential backoff manner.
105 | //
106 | // ### onClose hooks
107 | //
108 | // `onClose` hooks are invoked only in two cases. 1) the channel explicitly
109 | // closed on the server, or 2). The client explicitly closed, by calling
110 | // `channel.leave()`
111 | //
112 | //
113 | // ## Presence
114 | //
115 | // The `Presence` object provides features for syncing presence information
116 | // from the server with the client and handling presences joining and leaving.
117 | //
118 | // ### Syncing initial state from the server
119 | //
120 | // `Presence.syncState` is used to sync the list of presences on the server
121 | // with the client's state. An optional `onJoin` and `onLeave` callback can
122 | // be provided to react to changes in the client's local presences across
123 | // disconnects and reconnects with the server.
124 | //
125 | // `Presence.syncDiff` is used to sync a diff of presence join and leave
126 | // events from the server, as they happen. Like `syncState`, `syncDiff`
127 | // accepts optional `onJoin` and `onLeave` callbacks to react to a user
128 | // joining or leaving from a device.
129 | //
130 | // ### Listing Presences
131 | //
132 | // `Presence.list` is used to return a list of presence information
133 | // based on the local state of metadata. By default, all presence
134 | // metadata is returned, but a `listBy` function can be supplied to
135 | // allow the client to select which metadata to use for a given presence.
136 | // For example, you may have a user online from different devices with a
137 | // a metadata status of "online", but they have set themselves to "away"
138 | // on another device. In this case, they app may choose to use the "away"
139 | // status for what appears on the UI. The example below defines a `listBy`
140 | // function which prioritizes the first metadata which was registered for
141 | // each user. This could be the first tab they opened, or the first device
142 | // they came online from:
143 | //
144 | // let state = {}
145 | // state = Presence.syncState(state, stateFromServer)
146 | // let listBy = (id, {metas: [first, ...rest]}) => {
147 | // first.count = rest.length + 1 // count of this user's presences
148 | // first.id = id
149 | // return first
150 | // }
151 | // let onlineUsers = Presence.list(state, listBy)
152 | //
153 | //
154 | // ### Example Usage
155 | //
156 | // // detect if user has joined for the 1st time or from another tab/device
157 | // let onJoin = (id, current, newPres) => {
158 | // if(!current){
159 | // console.log("user has entered for the first time", newPres)
160 | // } else {
161 | // console.log("user additional presence", newPres)
162 | // }
163 | // }
164 | // // detect if user has left from all tabs/devices, or is still present
165 | // let onLeave = (id, current, leftPres) => {
166 | // if(current.metas.length === 0){
167 | // console.log("user has left from all devices", leftPres)
168 | // } else {
169 | // console.log("user left from a device", leftPres)
170 | // }
171 | // }
172 | // let presences = {} // client's initial empty presence state
173 | // // receive initial presence data from server, sent after join
174 | // myChannel.on("presences", state => {
175 | // presences = Presence.syncState(presences, state, onJoin, onLeave)
176 | // displayUsers(Presence.list(presences))
177 | // })
178 | // // receive "presence_diff" from server, containing join/leave events
179 | // myChannel.on("presence_diff", diff => {
180 | // presences = Presence.syncDiff(presences, diff, onJoin, onLeave)
181 | // this.setState({users: Presence.list(room.presences, listBy)})
182 | // })
183 | //
184 | var VSN = "1.0.0";
185 | var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };
186 | var DEFAULT_TIMEOUT = 10000;
187 | var CHANNEL_STATES = {
188 | closed: "closed",
189 | errored: "errored",
190 | joined: "joined",
191 | joining: "joining",
192 | leaving: "leaving"
193 | };
194 | var CHANNEL_EVENTS = {
195 | close: "phx_close",
196 | error: "phx_error",
197 | join: "phx_join",
198 | reply: "phx_reply",
199 | leave: "phx_leave"
200 | };
201 | var TRANSPORTS = {
202 | longpoll: "longpoll",
203 | websocket: "websocket"
204 | };
205 |
206 | var Push = function () {
207 |
208 | // Initializes the Push
209 | //
210 | // channel - The Channel
211 | // event - The event, for example `"phx_join"`
212 | // payload - The payload, for example `{user_id: 123}`
213 | // timeout - The push timeout in milliseconds
214 | //
215 |
216 | function Push(channel, event, payload, timeout) {
217 | _classCallCheck(this, Push);
218 |
219 | this.channel = channel;
220 | this.event = event;
221 | this.payload = payload || {};
222 | this.receivedResp = null;
223 | this.timeout = timeout;
224 | this.timeoutTimer = null;
225 | this.recHooks = [];
226 | this.sent = false;
227 | }
228 |
229 | _createClass(Push, [{
230 | key: "resend",
231 | value: function resend(timeout) {
232 | this.timeout = timeout;
233 | this.cancelRefEvent();
234 | this.ref = null;
235 | this.refEvent = null;
236 | this.receivedResp = null;
237 | this.sent = false;
238 | this.send();
239 | }
240 | }, {
241 | key: "send",
242 | value: function send() {
243 | if (this.hasReceived("timeout")) {
244 | return;
245 | }
246 | this.startTimeout();
247 | this.sent = true;
248 | this.channel.socket.push({
249 | topic: this.channel.topic,
250 | event: this.event,
251 | payload: this.payload,
252 | ref: this.ref
253 | });
254 | }
255 | }, {
256 | key: "receive",
257 | value: function receive(status, callback) {
258 | if (this.hasReceived(status)) {
259 | callback(this.receivedResp.response);
260 | }
261 |
262 | this.recHooks.push({ status: status, callback: callback });
263 | return this;
264 | }
265 |
266 | // private
267 |
268 | }, {
269 | key: "matchReceive",
270 | value: function matchReceive(_ref) {
271 | var status = _ref.status;
272 | var response = _ref.response;
273 | var ref = _ref.ref;
274 |
275 | this.recHooks.filter(function (h) {
276 | return h.status === status;
277 | }).forEach(function (h) {
278 | return h.callback(response);
279 | });
280 | }
281 | }, {
282 | key: "cancelRefEvent",
283 | value: function cancelRefEvent() {
284 | if (!this.refEvent) {
285 | return;
286 | }
287 | this.channel.off(this.refEvent);
288 | }
289 | }, {
290 | key: "cancelTimeout",
291 | value: function cancelTimeout() {
292 | clearTimeout(this.timeoutTimer);
293 | this.timeoutTimer = null;
294 | }
295 | }, {
296 | key: "startTimeout",
297 | value: function startTimeout() {
298 | var _this = this;
299 |
300 | if (this.timeoutTimer) {
301 | return;
302 | }
303 | this.ref = this.channel.socket.makeRef();
304 | this.refEvent = this.channel.replyEventName(this.ref);
305 |
306 | this.channel.on(this.refEvent, function (payload) {
307 | _this.cancelRefEvent();
308 | _this.cancelTimeout();
309 | _this.receivedResp = payload;
310 | _this.matchReceive(payload);
311 | });
312 |
313 | this.timeoutTimer = setTimeout(function () {
314 | _this.trigger("timeout", {});
315 | }, this.timeout);
316 | }
317 | }, {
318 | key: "hasReceived",
319 | value: function hasReceived(status) {
320 | return this.receivedResp && this.receivedResp.status === status;
321 | }
322 | }, {
323 | key: "trigger",
324 | value: function trigger(status, response) {
325 | this.channel.trigger(this.refEvent, { status: status, response: response });
326 | }
327 | }]);
328 |
329 | return Push;
330 | }();
331 |
332 | var Channel = exports.Channel = function () {
333 | function Channel(topic, params, socket) {
334 | var _this2 = this;
335 |
336 | _classCallCheck(this, Channel);
337 |
338 | this.state = CHANNEL_STATES.closed;
339 | this.topic = topic;
340 | this.params = params || {};
341 | this.socket = socket;
342 | this.bindings = [];
343 | this.timeout = this.socket.timeout;
344 | this.joinedOnce = false;
345 | this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout);
346 | this.pushBuffer = [];
347 | this.rejoinTimer = new Timer(function () {
348 | return _this2.rejoinUntilConnected();
349 | }, this.socket.reconnectAfterMs);
350 | this.joinPush.receive("ok", function () {
351 | _this2.state = CHANNEL_STATES.joined;
352 | _this2.rejoinTimer.reset();
353 | _this2.pushBuffer.forEach(function (pushEvent) {
354 | return pushEvent.send();
355 | });
356 | _this2.pushBuffer = [];
357 | });
358 | this.onClose(function () {
359 | _this2.rejoinTimer.reset();
360 | _this2.socket.log("channel", "close " + _this2.topic + " " + _this2.joinRef());
361 | _this2.state = CHANNEL_STATES.closed;
362 | _this2.socket.remove(_this2);
363 | });
364 | this.onError(function (reason) {
365 | if (_this2.isLeaving() || _this2.isClosed()) {
366 | return;
367 | }
368 | _this2.socket.log("channel", "error " + _this2.topic, reason);
369 | _this2.state = CHANNEL_STATES.errored;
370 | _this2.rejoinTimer.scheduleTimeout();
371 | });
372 | this.joinPush.receive("timeout", function () {
373 | if (!_this2.isJoining()) {
374 | return;
375 | }
376 | _this2.socket.log("channel", "timeout " + _this2.topic, _this2.joinPush.timeout);
377 | _this2.state = CHANNEL_STATES.errored;
378 | _this2.rejoinTimer.scheduleTimeout();
379 | });
380 | this.on(CHANNEL_EVENTS.reply, function (payload, ref) {
381 | _this2.trigger(_this2.replyEventName(ref), payload);
382 | });
383 | }
384 |
385 | _createClass(Channel, [{
386 | key: "rejoinUntilConnected",
387 | value: function rejoinUntilConnected() {
388 | this.rejoinTimer.scheduleTimeout();
389 | if (this.socket.isConnected()) {
390 | this.rejoin();
391 | }
392 | }
393 | }, {
394 | key: "join",
395 | value: function join() {
396 | var timeout = arguments.length <= 0 || arguments[0] === undefined ? this.timeout : arguments[0];
397 |
398 | if (this.joinedOnce) {
399 | throw "tried to join multiple times. 'join' can only be called a single time per channel instance";
400 | } else {
401 | this.joinedOnce = true;
402 | this.rejoin(timeout);
403 | return this.joinPush;
404 | }
405 | }
406 | }, {
407 | key: "onClose",
408 | value: function onClose(callback) {
409 | this.on(CHANNEL_EVENTS.close, callback);
410 | }
411 | }, {
412 | key: "onError",
413 | value: function onError(callback) {
414 | this.on(CHANNEL_EVENTS.error, function (reason) {
415 | return callback(reason);
416 | });
417 | }
418 | }, {
419 | key: "on",
420 | value: function on(event, callback) {
421 | this.bindings.push({ event: event, callback: callback });
422 | }
423 | }, {
424 | key: "off",
425 | value: function off(event) {
426 | this.bindings = this.bindings.filter(function (bind) {
427 | return bind.event !== event;
428 | });
429 | }
430 | }, {
431 | key: "canPush",
432 | value: function canPush() {
433 | return this.socket.isConnected() && this.isJoined();
434 | }
435 | }, {
436 | key: "push",
437 | value: function push(event, payload) {
438 | var timeout = arguments.length <= 2 || arguments[2] === undefined ? this.timeout : arguments[2];
439 |
440 | if (!this.joinedOnce) {
441 | throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use channel.join() before pushing events";
442 | }
443 | var pushEvent = new Push(this, event, payload, timeout);
444 | if (this.canPush()) {
445 | pushEvent.send();
446 | } else {
447 | pushEvent.startTimeout();
448 | this.pushBuffer.push(pushEvent);
449 | }
450 |
451 | return pushEvent;
452 | }
453 |
454 | // Leaves the channel
455 | //
456 | // Unsubscribes from server events, and
457 | // instructs channel to terminate on server
458 | //
459 | // Triggers onClose() hooks
460 | //
461 | // To receive leave acknowledgements, use the a `receive`
462 | // hook to bind to the server ack, ie:
463 | //
464 | // channel.leave().receive("ok", () => alert("left!") )
465 | //
466 |
467 | }, {
468 | key: "leave",
469 | value: function leave() {
470 | var _this3 = this;
471 |
472 | var timeout = arguments.length <= 0 || arguments[0] === undefined ? this.timeout : arguments[0];
473 |
474 | this.state = CHANNEL_STATES.leaving;
475 | var onClose = function onClose() {
476 | _this3.socket.log("channel", "leave " + _this3.topic);
477 | _this3.trigger(CHANNEL_EVENTS.close, "leave", _this3.joinRef());
478 | };
479 | var leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
480 | leavePush.receive("ok", function () {
481 | return onClose();
482 | }).receive("timeout", function () {
483 | return onClose();
484 | });
485 | leavePush.send();
486 | if (!this.canPush()) {
487 | leavePush.trigger("ok", {});
488 | }
489 |
490 | return leavePush;
491 | }
492 |
493 | // Overridable message hook
494 | //
495 | // Receives all events for specialized message handling
496 | // before dispatching to the channel callbacks.
497 | //
498 | // Must return the payload, modified or unmodified
499 |
500 | }, {
501 | key: "onMessage",
502 | value: function onMessage(event, payload, ref) {
503 | return payload;
504 | }
505 |
506 | // private
507 |
508 | }, {
509 | key: "isMember",
510 | value: function isMember(topic) {
511 | return this.topic === topic;
512 | }
513 | }, {
514 | key: "joinRef",
515 | value: function joinRef() {
516 | return this.joinPush.ref;
517 | }
518 | }, {
519 | key: "sendJoin",
520 | value: function sendJoin(timeout) {
521 | this.state = CHANNEL_STATES.joining;
522 | this.joinPush.resend(timeout);
523 | }
524 | }, {
525 | key: "rejoin",
526 | value: function rejoin() {
527 | var timeout = arguments.length <= 0 || arguments[0] === undefined ? this.timeout : arguments[0];
528 | if (this.isLeaving()) {
529 | return;
530 | }
531 | this.sendJoin(timeout);
532 | }
533 | }, {
534 | key: "trigger",
535 | value: function trigger(event, payload, ref) {
536 | var close = CHANNEL_EVENTS.close;
537 | var error = CHANNEL_EVENTS.error;
538 | var leave = CHANNEL_EVENTS.leave;
539 | var join = CHANNEL_EVENTS.join;
540 |
541 | if (ref && [close, error, leave, join].indexOf(event) >= 0 && ref !== this.joinRef()) {
542 | return;
543 | }
544 | var handledPayload = this.onMessage(event, payload, ref);
545 | if (payload && !handledPayload) {
546 | throw "channel onMessage callbacks must return the payload, modified or unmodified";
547 | }
548 |
549 | this.bindings.filter(function (bind) {
550 | return bind.event === event;
551 | }).map(function (bind) {
552 | return bind.callback(handledPayload, ref);
553 | });
554 | }
555 | }, {
556 | key: "replyEventName",
557 | value: function replyEventName(ref) {
558 | return "chan_reply_" + ref;
559 | }
560 | }, {
561 | key: "isClosed",
562 | value: function isClosed() {
563 | return this.state === CHANNEL_STATES.closed;
564 | }
565 | }, {
566 | key: "isErrored",
567 | value: function isErrored() {
568 | return this.state === CHANNEL_STATES.errored;
569 | }
570 | }, {
571 | key: "isJoined",
572 | value: function isJoined() {
573 | return this.state === CHANNEL_STATES.joined;
574 | }
575 | }, {
576 | key: "isJoining",
577 | value: function isJoining() {
578 | return this.state === CHANNEL_STATES.joining;
579 | }
580 | }, {
581 | key: "isLeaving",
582 | value: function isLeaving() {
583 | return this.state === CHANNEL_STATES.leaving;
584 | }
585 | }]);
586 |
587 | return Channel;
588 | }();
589 |
590 | var Socket = exports.Socket = function () {
591 |
592 | // Initializes the Socket
593 | //
594 | // endPoint - The string WebSocket endpoint, ie, "ws://example.com/ws",
595 | // "wss://example.com"
596 | // "/ws" (inherited host & protocol)
597 | // opts - Optional configuration
598 | // transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
599 | // Defaults to WebSocket with automatic LongPoll fallback.
600 | // timeout - The default timeout in milliseconds to trigger push timeouts.
601 | // Defaults `DEFAULT_TIMEOUT`
602 | // heartbeatIntervalMs - The millisec interval to send a heartbeat message
603 | // reconnectAfterMs - The optional function that returns the millsec
604 | // reconnect interval. Defaults to stepped backoff of:
605 | //
606 | // function(tries){
607 | // return [1000, 5000, 10000][tries - 1] || 10000
608 | // }
609 | //
610 | // logger - The optional function for specialized logging, ie:
611 | // `logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
612 | //
613 | // longpollerTimeout - The maximum timeout of a long poll AJAX request.
614 | // Defaults to 20s (double the server long poll timer).
615 | //
616 | // params - The optional params to pass when connecting
617 | //
618 | // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
619 | //
620 |
621 | function Socket(endPoint) {
622 | var _this4 = this;
623 |
624 | var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
625 |
626 | _classCallCheck(this, Socket);
627 |
628 | this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] };
629 | this.channels = [];
630 | this.sendBuffer = [];
631 | this.ref = 0;
632 | this.timeout = opts.timeout || DEFAULT_TIMEOUT;
633 | this.transport = opts.transport || window.WebSocket || LongPoll;
634 | this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000;
635 | this.reconnectAfterMs = opts.reconnectAfterMs || function (tries) {
636 | return [1000, 2000, 5000, 10000][tries - 1] || 10000;
637 | };
638 | this.logger = opts.logger || function () {}; // noop
639 | this.longpollerTimeout = opts.longpollerTimeout || 20000;
640 | this.params = opts.params || {};
641 | this.endPoint = endPoint + "/" + TRANSPORTS.websocket;
642 | this.reconnectTimer = new Timer(function () {
643 | _this4.disconnect(function () {
644 | return _this4.connect();
645 | });
646 | }, this.reconnectAfterMs);
647 | }
648 |
649 | _createClass(Socket, [{
650 | key: "protocol",
651 | value: function protocol() {
652 | return location.protocol.match(/^https/) ? "wss" : "ws";
653 | }
654 | }, {
655 | key: "endPointURL",
656 | value: function endPointURL() {
657 | var uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params), { vsn: VSN });
658 | if (uri.charAt(0) !== "/") {
659 | return uri;
660 | }
661 | if (uri.charAt(1) === "/") {
662 | return this.protocol() + ":" + uri;
663 | }
664 |
665 | return this.protocol() + "://" + location.host + uri;
666 | }
667 | }, {
668 | key: "disconnect",
669 | value: function disconnect(callback, code, reason) {
670 | if (this.conn) {
671 | this.conn.onclose = function () {}; // noop
672 | if (code) {
673 | this.conn.close(code, reason || "");
674 | } else {
675 | this.conn.close();
676 | }
677 | this.conn = null;
678 | }
679 | callback && callback();
680 | }
681 |
682 | // params - The params to send when connecting, for example `{user_id: userToken}`
683 |
684 | }, {
685 | key: "connect",
686 | value: function connect(params) {
687 | var _this5 = this;
688 |
689 | if (params) {
690 | console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor");
691 | this.params = params;
692 | }
693 | if (this.conn) {
694 | return;
695 | }
696 |
697 | this.conn = new this.transport(this.endPointURL());
698 | this.conn.timeout = this.longpollerTimeout;
699 | this.conn.onopen = function () {
700 | return _this5.onConnOpen();
701 | };
702 | this.conn.onerror = function (error) {
703 | return _this5.onConnError(error);
704 | };
705 | this.conn.onmessage = function (event) {
706 | return _this5.onConnMessage(event);
707 | };
708 | this.conn.onclose = function (event) {
709 | return _this5.onConnClose(event);
710 | };
711 | }
712 |
713 | // Logs the message. Override `this.logger` for specialized logging. noops by default
714 |
715 | }, {
716 | key: "log",
717 | value: function log(kind, msg, data) {
718 | this.logger(kind, msg, data);
719 | }
720 |
721 | // Registers callbacks for connection state change events
722 | //
723 | // Examples
724 | //
725 | // socket.onError(function(error){ alert("An error occurred") })
726 | //
727 |
728 | }, {
729 | key: "onOpen",
730 | value: function onOpen(callback) {
731 | this.stateChangeCallbacks.open.push(callback);
732 | }
733 | }, {
734 | key: "onClose",
735 | value: function onClose(callback) {
736 | this.stateChangeCallbacks.close.push(callback);
737 | }
738 | }, {
739 | key: "onError",
740 | value: function onError(callback) {
741 | this.stateChangeCallbacks.error.push(callback);
742 | }
743 | }, {
744 | key: "onMessage",
745 | value: function onMessage(callback) {
746 | this.stateChangeCallbacks.message.push(callback);
747 | }
748 | }, {
749 | key: "onConnOpen",
750 | value: function onConnOpen() {
751 | var _this6 = this;
752 |
753 | this.log("transport", "connected to " + this.endPointURL(), this.transport.prototype);
754 | this.flushSendBuffer();
755 | this.reconnectTimer.reset();
756 | if (!this.conn.skipHeartbeat) {
757 | clearInterval(this.heartbeatTimer);
758 | this.heartbeatTimer = setInterval(function () {
759 | return _this6.sendHeartbeat();
760 | }, this.heartbeatIntervalMs);
761 | }
762 | this.stateChangeCallbacks.open.forEach(function (callback) {
763 | return callback();
764 | });
765 | }
766 | }, {
767 | key: "onConnClose",
768 | value: function onConnClose(event) {
769 | this.log("transport", "close", event);
770 | this.triggerChanError();
771 | clearInterval(this.heartbeatTimer);
772 | this.reconnectTimer.scheduleTimeout();
773 | this.stateChangeCallbacks.close.forEach(function (callback) {
774 | return callback(event);
775 | });
776 | }
777 | }, {
778 | key: "onConnError",
779 | value: function onConnError(error) {
780 | this.log("transport", error);
781 | this.triggerChanError();
782 | this.stateChangeCallbacks.error.forEach(function (callback) {
783 | return callback(error);
784 | });
785 | }
786 | }, {
787 | key: "triggerChanError",
788 | value: function triggerChanError() {
789 | this.channels.forEach(function (channel) {
790 | return channel.trigger(CHANNEL_EVENTS.error);
791 | });
792 | }
793 | }, {
794 | key: "connectionState",
795 | value: function connectionState() {
796 | switch (this.conn && this.conn.readyState) {
797 | case SOCKET_STATES.connecting:
798 | return "connecting";
799 | case SOCKET_STATES.open:
800 | return "open";
801 | case SOCKET_STATES.closing:
802 | return "closing";
803 | default:
804 | return "closed";
805 | }
806 | }
807 | }, {
808 | key: "isConnected",
809 | value: function isConnected() {
810 | return this.connectionState() === "open";
811 | }
812 | }, {
813 | key: "remove",
814 | value: function remove(channel) {
815 | this.channels = this.channels.filter(function (c) {
816 | return c.joinRef() !== channel.joinRef();
817 | });
818 | }
819 | }, {
820 | key: "channel",
821 | value: function channel(topic) {
822 | var chanParams = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
823 |
824 | var chan = new Channel(topic, chanParams, this);
825 | this.channels.push(chan);
826 | return chan;
827 | }
828 | }, {
829 | key: "push",
830 | value: function push(data) {
831 | var _this7 = this;
832 |
833 | var topic = data.topic;
834 | var event = data.event;
835 | var payload = data.payload;
836 | var ref = data.ref;
837 |
838 | var callback = function callback() {
839 | return _this7.conn.send(JSON.stringify(data));
840 | };
841 | this.log("push", topic + " " + event + " (" + ref + ")", payload);
842 | if (this.isConnected()) {
843 | callback();
844 | } else {
845 | this.sendBuffer.push(callback);
846 | }
847 | }
848 |
849 | // Return the next message ref, accounting for overflows
850 |
851 | }, {
852 | key: "makeRef",
853 | value: function makeRef() {
854 | var newRef = this.ref + 1;
855 | if (newRef === this.ref) {
856 | this.ref = 0;
857 | } else {
858 | this.ref = newRef;
859 | }
860 |
861 | return this.ref.toString();
862 | }
863 | }, {
864 | key: "sendHeartbeat",
865 | value: function sendHeartbeat() {
866 | if (!this.isConnected()) {
867 | return;
868 | }
869 | this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.makeRef() });
870 | }
871 | }, {
872 | key: "flushSendBuffer",
873 | value: function flushSendBuffer() {
874 | if (this.isConnected() && this.sendBuffer.length > 0) {
875 | this.sendBuffer.forEach(function (callback) {
876 | return callback();
877 | });
878 | this.sendBuffer = [];
879 | }
880 | }
881 | }, {
882 | key: "onConnMessage",
883 | value: function onConnMessage(rawMessage) {
884 | var msg = JSON.parse(rawMessage.data);
885 | var topic = msg.topic;
886 | var event = msg.event;
887 | var payload = msg.payload;
888 | var ref = msg.ref;
889 |
890 | this.log("receive", (payload.status || "") + " " + topic + " " + event + " " + (ref && "(" + ref + ")" || ""), payload);
891 | this.channels.filter(function (channel) {
892 | return channel.isMember(topic);
893 | }).forEach(function (channel) {
894 | return channel.trigger(event, payload, ref);
895 | });
896 | this.stateChangeCallbacks.message.forEach(function (callback) {
897 | return callback(msg);
898 | });
899 | }
900 | }]);
901 |
902 | return Socket;
903 | }();
904 |
905 | var LongPoll = exports.LongPoll = function () {
906 | function LongPoll(endPoint) {
907 | _classCallCheck(this, LongPoll);
908 |
909 | this.endPoint = null;
910 | this.token = null;
911 | this.skipHeartbeat = true;
912 | this.onopen = function () {}; // noop
913 | this.onerror = function () {}; // noop
914 | this.onmessage = function () {}; // noop
915 | this.onclose = function () {}; // noop
916 | this.pollEndpoint = this.normalizeEndpoint(endPoint);
917 | this.readyState = SOCKET_STATES.connecting;
918 |
919 | this.poll();
920 | }
921 |
922 | _createClass(LongPoll, [{
923 | key: "normalizeEndpoint",
924 | value: function normalizeEndpoint(endPoint) {
925 | return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)\/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
926 | }
927 | }, {
928 | key: "endpointURL",
929 | value: function endpointURL() {
930 | return Ajax.appendParams(this.pollEndpoint, { token: this.token });
931 | }
932 | }, {
933 | key: "closeAndRetry",
934 | value: function closeAndRetry() {
935 | this.close();
936 | this.readyState = SOCKET_STATES.connecting;
937 | }
938 | }, {
939 | key: "ontimeout",
940 | value: function ontimeout() {
941 | this.onerror("timeout");
942 | this.closeAndRetry();
943 | }
944 | }, {
945 | key: "poll",
946 | value: function poll() {
947 | var _this8 = this;
948 |
949 | if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) {
950 | return;
951 | }
952 |
953 | Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) {
954 | if (resp) {
955 | var status = resp.status;
956 | var token = resp.token;
957 | var messages = resp.messages;
958 |
959 | _this8.token = token;
960 | } else {
961 | var status = 0;
962 | }
963 |
964 | switch (status) {
965 | case 200:
966 | messages.forEach(function (msg) {
967 | return _this8.onmessage({ data: JSON.stringify(msg) });
968 | });
969 | _this8.poll();
970 | break;
971 | case 204:
972 | _this8.poll();
973 | break;
974 | case 410:
975 | _this8.readyState = SOCKET_STATES.open;
976 | _this8.onopen();
977 | _this8.poll();
978 | break;
979 | case 0:
980 | case 500:
981 | _this8.onerror();
982 | _this8.closeAndRetry();
983 | break;
984 | default:
985 | throw "unhandled poll status " + status;
986 | }
987 | });
988 | }
989 | }, {
990 | key: "send",
991 | value: function send(body) {
992 | var _this9 = this;
993 |
994 | Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) {
995 | if (!resp || resp.status !== 200) {
996 | _this9.onerror(status);
997 | _this9.closeAndRetry();
998 | }
999 | });
1000 | }
1001 | }, {
1002 | key: "close",
1003 | value: function close(code, reason) {
1004 | this.readyState = SOCKET_STATES.closed;
1005 | this.onclose();
1006 | }
1007 | }]);
1008 |
1009 | return LongPoll;
1010 | }();
1011 |
1012 | var Ajax = exports.Ajax = function () {
1013 | function Ajax() {
1014 | _classCallCheck(this, Ajax);
1015 | }
1016 |
1017 | _createClass(Ajax, null, [{
1018 | key: "request",
1019 | value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) {
1020 | if (window.XDomainRequest) {
1021 | var req = new XDomainRequest(); // IE8, IE9
1022 | this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
1023 | } else {
1024 | var req = window.XMLHttpRequest ? new XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
1025 | new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5
1026 | this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback);
1027 | }
1028 | }
1029 | }, {
1030 | key: "xdomainRequest",
1031 | value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {
1032 | var _this10 = this;
1033 |
1034 | req.timeout = timeout;
1035 | req.open(method, endPoint);
1036 | req.onload = function () {
1037 | var response = _this10.parseJSON(req.responseText);
1038 | callback && callback(response);
1039 | };
1040 | if (ontimeout) {
1041 | req.ontimeout = ontimeout;
1042 | }
1043 |
1044 | // Work around bug in IE9 that requires an attached onprogress handler
1045 | req.onprogress = function () {};
1046 |
1047 | req.send(body);
1048 | }
1049 | }, {
1050 | key: "xhrRequest",
1051 | value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) {
1052 | var _this11 = this;
1053 |
1054 | req.timeout = timeout;
1055 | req.open(method, endPoint, true);
1056 | req.setRequestHeader("Content-Type", accept);
1057 | req.onerror = function () {
1058 | callback && callback(null);
1059 | };
1060 | req.onreadystatechange = function () {
1061 | if (req.readyState === _this11.states.complete && callback) {
1062 | var response = _this11.parseJSON(req.responseText);
1063 | callback(response);
1064 | }
1065 | };
1066 | if (ontimeout) {
1067 | req.ontimeout = ontimeout;
1068 | }
1069 |
1070 | req.send(body);
1071 | }
1072 | }, {
1073 | key: "parseJSON",
1074 | value: function parseJSON(resp) {
1075 | return resp && resp !== "" ? JSON.parse(resp) : null;
1076 | }
1077 | }, {
1078 | key: "serialize",
1079 | value: function serialize(obj, parentKey) {
1080 | var queryStr = [];
1081 | for (var key in obj) {
1082 | if (!obj.hasOwnProperty(key)) {
1083 | continue;
1084 | }
1085 | var paramKey = parentKey ? parentKey + "[" + key + "]" : key;
1086 | var paramVal = obj[key];
1087 | if ((typeof paramVal === "undefined" ? "undefined" : _typeof(paramVal)) === "object") {
1088 | queryStr.push(this.serialize(paramVal, paramKey));
1089 | } else {
1090 | queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal));
1091 | }
1092 | }
1093 | return queryStr.join("&");
1094 | }
1095 | }, {
1096 | key: "appendParams",
1097 | value: function appendParams(url, params) {
1098 | if (Object.keys(params).length === 0) {
1099 | return url;
1100 | }
1101 |
1102 | var prefix = url.match(/\?/) ? "&" : "?";
1103 | return "" + url + prefix + this.serialize(params);
1104 | }
1105 | }]);
1106 |
1107 | return Ajax;
1108 | }();
1109 |
1110 | Ajax.states = { complete: 4 };
1111 |
1112 | var Presence = exports.Presence = {
1113 | syncState: function syncState(currentState, newState, onJoin, onLeave) {
1114 | var _this12 = this;
1115 |
1116 | var state = this.clone(currentState);
1117 | var joins = {};
1118 | var leaves = {};
1119 |
1120 | this.map(state, function (key, presence) {
1121 | if (!newState[key]) {
1122 | leaves[key] = presence;
1123 | }
1124 | });
1125 | this.map(newState, function (key, newPresence) {
1126 | var currentPresence = state[key];
1127 | if (currentPresence) {
1128 | (function () {
1129 | var newRefs = newPresence.metas.map(function (m) {
1130 | return m.phx_ref;
1131 | });
1132 | var curRefs = currentPresence.metas.map(function (m) {
1133 | return m.phx_ref;
1134 | });
1135 | var joinedMetas = newPresence.metas.filter(function (m) {
1136 | return curRefs.indexOf(m.phx_ref) < 0;
1137 | });
1138 | var leftMetas = currentPresence.metas.filter(function (m) {
1139 | return newRefs.indexOf(m.phx_ref) < 0;
1140 | });
1141 | if (joinedMetas.length > 0) {
1142 | joins[key] = newPresence;
1143 | joins[key].metas = joinedMetas;
1144 | }
1145 | if (leftMetas.length > 0) {
1146 | leaves[key] = _this12.clone(currentPresence);
1147 | leaves[key].metas = leftMetas;
1148 | }
1149 | })();
1150 | } else {
1151 | joins[key] = newPresence;
1152 | }
1153 | });
1154 | return this.syncDiff(state, { joins: joins, leaves: leaves }, onJoin, onLeave);
1155 | },
1156 | syncDiff: function syncDiff(currentState, _ref2, onJoin, onLeave) {
1157 | var joins = _ref2.joins;
1158 | var leaves = _ref2.leaves;
1159 |
1160 | var state = this.clone(currentState);
1161 | if (!onJoin) {
1162 | onJoin = function onJoin() {};
1163 | }
1164 | if (!onLeave) {
1165 | onLeave = function onLeave() {};
1166 | }
1167 |
1168 | this.map(joins, function (key, newPresence) {
1169 | var currentPresence = state[key];
1170 | state[key] = newPresence;
1171 | if (currentPresence) {
1172 | var _state$key$metas;
1173 |
1174 | (_state$key$metas = state[key].metas).unshift.apply(_state$key$metas, _toConsumableArray(currentPresence.metas));
1175 | }
1176 | onJoin(key, currentPresence, newPresence);
1177 | });
1178 | this.map(leaves, function (key, leftPresence) {
1179 | var currentPresence = state[key];
1180 | if (!currentPresence) {
1181 | return;
1182 | }
1183 | var refsToRemove = leftPresence.metas.map(function (m) {
1184 | return m.phx_ref;
1185 | });
1186 | currentPresence.metas = currentPresence.metas.filter(function (p) {
1187 | return refsToRemove.indexOf(p.phx_ref) < 0;
1188 | });
1189 | onLeave(key, currentPresence, leftPresence);
1190 | if (currentPresence.metas.length === 0) {
1191 | delete state[key];
1192 | }
1193 | });
1194 | return state;
1195 | },
1196 | list: function list(presences, chooser) {
1197 | if (!chooser) {
1198 | chooser = function chooser(key, pres) {
1199 | return pres;
1200 | };
1201 | }
1202 |
1203 | return this.map(presences, function (key, presence) {
1204 | return chooser(key, presence);
1205 | });
1206 | },
1207 |
1208 | // private
1209 |
1210 | map: function map(obj, func) {
1211 | return Object.getOwnPropertyNames(obj).map(function (key) {
1212 | return func(key, obj[key]);
1213 | });
1214 | },
1215 | clone: function clone(obj) {
1216 | return JSON.parse(JSON.stringify(obj));
1217 | }
1218 | };
1219 |
1220 | // Creates a timer that accepts a `timerCalc` function to perform
1221 | // calculated timeout retries, such as exponential backoff.
1222 | //
1223 | // ## Examples
1224 | //
1225 | // let reconnectTimer = new Timer(() => this.connect(), function(tries){
1226 | // return [1000, 5000, 10000][tries - 1] || 10000
1227 | // })
1228 | // reconnectTimer.scheduleTimeout() // fires after 1000
1229 | // reconnectTimer.scheduleTimeout() // fires after 5000
1230 | // reconnectTimer.reset()
1231 | // reconnectTimer.scheduleTimeout() // fires after 1000
1232 | //
1233 |
1234 | var Timer = function () {
1235 | function Timer(callback, timerCalc) {
1236 | _classCallCheck(this, Timer);
1237 |
1238 | this.callback = callback;
1239 | this.timerCalc = timerCalc;
1240 | this.timer = null;
1241 | this.tries = 0;
1242 | }
1243 |
1244 | _createClass(Timer, [{
1245 | key: "reset",
1246 | value: function reset() {
1247 | this.tries = 0;
1248 | clearTimeout(this.timer);
1249 | }
1250 |
1251 | // Cancels any previous scheduleTimeout and schedules callback
1252 |
1253 | }, {
1254 | key: "scheduleTimeout",
1255 | value: function scheduleTimeout() {
1256 | var _this13 = this;
1257 |
1258 | clearTimeout(this.timer);
1259 |
1260 | this.timer = setTimeout(function () {
1261 | _this13.tries = _this13.tries + 1;
1262 | _this13.callback();
1263 | }, this.timerCalc(this.tries + 1));
1264 | }
1265 | }]);
1266 |
1267 | return Timer;
1268 | }();
1269 |
1270 | })(typeof(exports) === "undefined" ? window.Phoenix = window.Phoenix || {} : exports);
1271 |
1272 |
--------------------------------------------------------------------------------