├── README.md ├── zero-django ├── .gitignore ├── .python-version ├── README.md ├── db.sqlite3 ├── manage.py ├── people │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_person_friends.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── requirements.txt └── zero_django │ ├── __init__.py │ ├── models.py │ ├── schema.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── zero-node ├── .gitignore ├── README.md ├── index.js ├── package.json └── schema.js ├── zero-phoenix ├── .gitignore ├── README.md ├── config │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ └── test.exs ├── lib │ ├── zero_phoenix.ex │ └── zero_phoenix │ │ ├── endpoint.ex │ │ └── repo.ex ├── mix.exs ├── mix.lock ├── priv │ ├── gettext │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── errors.po │ │ └── errors.pot │ ├── repo │ │ ├── migrations │ │ │ ├── 20160730004705_create_person.exs │ │ │ └── 20160730024335_create_friendship.exs │ │ └── seeds.exs │ └── static │ │ ├── css │ │ └── app.css │ │ ├── favicon.ico │ │ ├── images │ │ └── phoenix.png │ │ ├── js │ │ ├── app.js │ │ └── phoenix.js │ │ └── robots.txt ├── test │ ├── 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 │ ├── test_helper.exs │ └── views │ │ ├── error_view_test.exs │ │ ├── layout_view_test.exs │ │ └── page_view_test.exs └── web │ ├── channels │ └── user_socket.ex │ ├── controllers │ ├── page_controller.ex │ └── person_controller.ex │ ├── gettext.ex │ ├── graphql │ ├── schema.ex │ └── types │ │ └── person.ex │ ├── models │ ├── friendship.ex │ └── person.ex │ ├── resolvers │ └── person_resolver.ex │ ├── router.ex │ ├── templates │ ├── layout │ │ └── app.html.eex │ └── page │ │ └── index.html.eex │ ├── views │ ├── changeset_view.ex │ ├── error_helpers.ex │ ├── error_view.ex │ ├── layout_view.ex │ ├── page_view.ex │ └── person_view.ex │ └── web.ex ├── zero-rails ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ ├── application.js │ │ │ ├── cable.coffee │ │ │ └── channels │ │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── graphql_controller.rb │ │ └── people_controller.rb │ ├── helpers │ │ ├── application_helper.rb │ │ └── people_helper.rb │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── person.rb │ │ └── schema.rb │ ├── serializers │ │ └── person_serializer.rb │ └── views │ │ └── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb ├── bin │ ├── bundle │ ├── rails │ ├── rake │ ├── setup │ ├── spring │ └── update ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── active_record_belongs_to_required_by_default.rb │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── callback_terminator.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── per_form_csrf_tokens.rb │ │ ├── request_forgery_protection.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ └── secrets.yml ├── db │ ├── migrate │ │ ├── 20160413032518_create_people.rb │ │ └── 20160413033451_create_friendships.rb │ ├── schema.rb │ └── seeds.rb ├── lib │ ├── assets │ │ └── .keep │ └── tasks │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ └── robots.txt ├── test │ ├── controllers │ │ └── .keep │ ├── fixtures │ │ ├── .keep │ │ └── files │ │ │ └── .keep │ ├── helpers │ │ └── .keep │ ├── integration │ │ └── .keep │ ├── mailers │ │ └── .keep │ ├── models │ │ └── .keep │ └── test_helper.rb ├── tmp │ └── .keep └── vendor │ └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep └── zero-scala ├── .gitignore ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ ├── application.conf │ └── graphiql.html └── scala │ ├── Repository.scala │ ├── SchemaDefinition.scala │ └── Server.scala └── test ├── resources └── application.conf └── scala └── SchemaSpec.scala /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-django/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /zero-django/.python-version: -------------------------------------------------------------------------------- 1 | 3.5.0 2 | -------------------------------------------------------------------------------- /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-django/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveluscher/zero-to-graphql/3daf2d8dc2ac9bc63cf16966d6489fe43cbddbe9/zero-django/db.sqlite3 -------------------------------------------------------------------------------- /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-django/people/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveluscher/zero-to-graphql/3daf2d8dc2ac9bc63cf16966d6489fe43cbddbe9/zero-django/people/__init__.py -------------------------------------------------------------------------------- /zero-django/people/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /zero-django/people/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PeopleConfig(AppConfig): 5 | name = 'people' 6 | -------------------------------------------------------------------------------- /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-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-django/people/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveluscher/zero-to-graphql/3daf2d8dc2ac9bc63cf16966d6489fe43cbddbe9/zero-django/people/migrations/__init__.py -------------------------------------------------------------------------------- /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-django/people/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /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-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-django/requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.9 2 | graphene[django] 3 | django_graphiql 4 | -------------------------------------------------------------------------------- /zero-django/zero_django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveluscher/zero-to-graphql/3daf2d8dc2ac9bc63cf16966d6489fe43cbddbe9/zero-django/zero_django/__init__.py -------------------------------------------------------------------------------- /zero-django/zero_django/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveluscher/zero-to-graphql/3daf2d8dc2ac9bc63cf16966d6489fe43cbddbe9/zero-django/zero_django/models.py -------------------------------------------------------------------------------- /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-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-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-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-node/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /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-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-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-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-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-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/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/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/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-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-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/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/lib/zero_phoenix/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule ZeroPhoenix.Repo do 2 | use Ecto.Repo, otp_app: :zero_phoenix 3 | end 4 | -------------------------------------------------------------------------------- /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/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/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/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-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-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/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveluscher/zero-to-graphql/3daf2d8dc2ac9bc63cf16966d6489fe43cbddbe9/zero-phoenix/priv/static/favicon.ico -------------------------------------------------------------------------------- /zero-phoenix/priv/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveluscher/zero-to-graphql/3daf2d8dc2ac9bc63cf16966d6489fe43cbddbe9/zero-phoenix/priv/static/images/phoenix.png -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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-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-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-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-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-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-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/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-phoenix/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(ZeroPhoenix.Repo, :manual) 4 | 5 | -------------------------------------------------------------------------------- /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-phoenix/test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ZeroPhoenix.LayoutViewTest do 2 | use ZeroPhoenix.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /zero-phoenix/test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ZeroPhoenix.PageViewTest do 2 | use ZeroPhoenix.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /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-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-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-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-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-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-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-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-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-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/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |<%= get_flash(@conn, :info) %>
26 |<%= get_flash(@conn, :error) %>
27 | 28 |A productive web framework that
does not compromise speed and maintainability.
You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |