├── .formatter.exs
├── .gitignore
├── README.md
├── client
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── NewUser.js
│ ├── Subscriber.js
│ ├── Users.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── serviceWorker.js
│ └── util
│ │ ├── absinthe-socket-link.js
│ │ └── apollo.js
└── yarn.lock
├── config
├── config.exs
├── dev.exs
├── prod.exs
├── prod.secret.exs
└── test.exs
├── lib
├── my_cool_app.ex
├── my_cool_app
│ ├── accounts.ex
│ ├── accounts
│ │ └── user.ex
│ ├── application.ex
│ └── repo.ex
├── my_cool_app_web.ex
└── my_cool_app_web
│ ├── channels
│ ├── absinthe_socket.ex
│ └── user_socket.ex
│ ├── controllers
│ └── page_controller.ex
│ ├── endpoint.ex
│ ├── gettext.ex
│ ├── resolvers
│ └── account_resolver.ex
│ ├── router.ex
│ ├── schema.ex
│ ├── schema
│ └── account_types.ex
│ ├── templates
│ ├── layout
│ │ └── app.html.eex
│ └── page
│ │ └── index.html.eex
│ └── views
│ ├── error_helpers.ex
│ ├── error_view.ex
│ ├── layout_view.ex
│ └── page_view.ex
├── mix.exs
├── mix.lock
├── priv
├── gettext
│ ├── en
│ │ └── LC_MESSAGES
│ │ │ └── errors.po
│ └── errors.pot
└── repo
│ ├── migrations
│ ├── .formatter.exs
│ └── 20190705201815_create_users.exs
│ └── seeds.exs
└── test
├── my_cool_app
└── accounts_test.exs
├── my_cool_app_web
├── controllers
│ └── page_controller_test.exs
└── views
│ ├── error_view_test.exs
│ ├── layout_view_test.exs
│ └── page_view_test.exs
├── support
├── channel_case.ex
├── conn_case.ex
└── data_case.ex
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto, :phoenix],
3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
4 | subdirectories: ["priv/*/migrations"]
5 | ]
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | my_cool_app-*.tar
24 |
25 | # If NPM crashes, it generates a log, let's ignore it too.
26 | npm-debug.log
27 |
28 | # The directory NPM downloads your dependencies sources to.
29 | /assets/node_modules/
30 |
31 | # Since we are building assets from assets/,
32 | # we ignore priv/static. You may want to comment
33 | # this depending on your deployment strategy.
34 | /priv/static/
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MyCoolApp
2 |
3 | This is the end result of the corresponding tutorial here: https://www.viget.com/articles/getting-started-with-graphql-phoenix-and-react
4 |
5 | To start your Phoenix server:
6 |
7 | * Install dependencies with `mix deps.get`
8 | * Create and migrate your database with `mix ecto.setup`
9 | * Install Node.js dependencies with `cd assets && npm install`
10 | * Start Phoenix endpoint with `mix phx.server`
11 |
12 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
13 |
14 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@absinthe/socket": "^0.2.1",
7 | "@absinthe/socket-apollo-link": "^0.2.1",
8 | "apollo-boost": "^0.4.0",
9 | "graphql": "^14.3.1",
10 | "graphql-tag": "^2.10.1",
11 | "phoenix": "^1.4.3",
12 | "react": "^16.8.6",
13 | "react-apollo": "^2.5.6",
14 | "react-dom": "^16.8.6",
15 | "react-scripts": "3.0.1"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test",
21 | "eject": "react-scripts eject"
22 | },
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lilwillifo/my_cool_app/294c27e851ac6887cacb5236bbe8564e9bc15b92/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ApolloProvider } from "react-apollo";
3 | import "./App.css";
4 | import { createClient } from "./util/apollo";
5 | import Users from "./Users";
6 |
7 | function App() {
8 | const client = createClient();
9 |
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default App;
18 |
--------------------------------------------------------------------------------
/client/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/client/src/NewUser.js:
--------------------------------------------------------------------------------
1 | import React, { useState} from "react";
2 | import gql from "graphql-tag";
3 | import { Mutation } from "react-apollo"
4 |
5 | const CREATE_USER = gql`
6 | mutation CreateUser($name: String!, $email: String) {
7 | createUser(name: $name, email: $email) {
8 | id
9 | }
10 | }
11 | `;
12 |
13 | const NewUser = ({ params }) => {
14 | const [name, setName] = useState("");
15 | const [email, setEmail] = useState("");
16 |
17 | const mutation = CREATE_USER;
18 |
19 | return (
20 | {
22 | setName("");
23 | setEmail("");
24 | }}
25 | >
26 | {(submit, { data, loading, error }) => {
27 | return (
28 |
50 | );
51 | }}
52 |
53 | );
54 | };
55 |
56 | export default NewUser
57 |
--------------------------------------------------------------------------------
/client/src/Subscriber.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | const Subscriber = ({ subscribeToNew, children }) => {
4 | useEffect(() => {
5 | subscribeToNew();
6 | }, []); // eslint-disable-line react-hooks/exhaustive-deps
7 |
8 | return children;
9 | };
10 |
11 | export default Subscriber;
12 |
--------------------------------------------------------------------------------
/client/src/Users.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | import React from "react";
3 | import { Query } from "react-apollo";
4 | import produce from "immer";
5 | import Subscriber from "./Subscriber";
6 | import NewUser from "./NewUser";
7 |
8 | function Users({subscribeToNew, newItemPosition, createParams}) {
9 | const LIST_USERS = gql`
10 | {
11 | listUsers {
12 | id
13 | name
14 | email
15 | }
16 | }
17 | `;
18 |
19 | const USERS_SUBSCRIPTION = gql`
20 | subscription onUserCreated {
21 | userCreated {
22 | id
23 | name
24 | email
25 | }
26 | }
27 | `
28 |
29 | return (
30 |
31 |
Users!
32 |
33 | {({ loading, error, data, subscribeToMore }) => {
34 | if (loading) return "Loading...";
35 | if (error) return `Error! ${error.message}`;
36 |
37 | return (
38 | <>
39 |
40 |
41 | subscribeToMore({
42 | document: USERS_SUBSCRIPTION,
43 | updateQuery: (prev, { subscriptionData }) => {
44 | if (!subscriptionData.data) return prev;
45 | const newUser = subscriptionData.data.userCreated;
46 | // Check that we don't already have the
47 | // post stored.
48 |
49 | if (prev.listUsers.find((user) => user.id === newUser.id)) {
50 | return prev;
51 | }
52 |
53 | return produce(prev, (next) => {
54 | next.listUsers.unshift(newUser);
55 | });
56 | },
57 | })
58 | }>
59 |
60 | {data.listUsers.map(user => (
61 | -
62 | {user.name}: {user.email}
63 |
64 | ))}
65 |
66 |
67 | >
68 | );
69 | }}
70 |
71 |
72 | );
73 | }
74 | export default Users;
75 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/client/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/client/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/client/src/util/absinthe-socket-link.js:
--------------------------------------------------------------------------------
1 | import * as AbsintheSocket from "@absinthe/socket";
2 | import {createAbsintheSocketLink} from "@absinthe/socket-apollo-link";
3 | import {Socket as PhoenixSocket} from "phoenix";
4 |
5 | export default createAbsintheSocketLink(AbsintheSocket.create(
6 | new PhoenixSocket("ws://localhost:4000/socket")
7 | ));
8 |
--------------------------------------------------------------------------------
/client/src/util/apollo.js:
--------------------------------------------------------------------------------
1 | import { InMemoryCache } from "apollo-cache-inmemory";
2 | import { ApolloClient } from "apollo-client";
3 | import { HttpLink } from "apollo-link-http";
4 | import absintheSocketLink from "./absinthe-socket-link"
5 | import { split } from "apollo-link";
6 | import { hasSubscription } from "@jumpn/utils-graphql";
7 |
8 | const HTTP_URI = "http://localhost:4000";
9 |
10 | const link = split(
11 | operation => hasSubscription(operation.query),
12 | absintheSocketLink,
13 | new HttpLink({ uri: HTTP_URI })
14 | );
15 |
16 | export const createClient = () => {
17 | return new ApolloClient({
18 | link: link,
19 | cache: new InMemoryCache()
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/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 |
7 | # General application configuration
8 | use Mix.Config
9 |
10 | config :my_cool_app,
11 | ecto_repos: [MyCoolApp.Repo]
12 |
13 | # Configures the endpoint
14 | config :my_cool_app, MyCoolAppWeb.Endpoint,
15 | url: [host: "localhost"],
16 | secret_key_base: "Aq4qU9deLvYYn2N9RZizsmPyvdz6wZqeH8+BPa1C6Um23FkGmjiy2om/RJZ1PCMG",
17 | render_errors: [view: MyCoolAppWeb.ErrorView, accepts: ~w(html json)],
18 | pubsub: [name: MyCoolApp.PubSub, 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 | # Use Jason for JSON parsing in Phoenix
26 | config :phoenix, :json_library, Jason
27 |
28 | # Import environment specific config. This must remain at the bottom
29 | # of this file so it overrides the configuration defined above.
30 | import_config "#{Mix.env()}.exs"
31 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure your database
4 | config :my_cool_app, MyCoolApp.Repo,
5 | username: "postgres",
6 | password: "postgres",
7 | database: "my_cool_app_dev",
8 | hostname: "localhost",
9 | show_sensitive_data_on_connection_error: true,
10 | pool_size: 10
11 |
12 | # For development, we disable any cache and enable
13 | # debugging and code reloading.
14 | #
15 | # The watchers configuration can be used to run external
16 | # watchers to your application. For example, we use it
17 | # with webpack to recompile .js and .css sources.
18 | config :my_cool_app, MyCoolAppWeb.Endpoint,
19 | http: [port: 4000],
20 | debug_errors: true,
21 | code_reloader: true,
22 | check_origin: false,
23 | watchers: [
24 | node: [
25 | "node_modules/webpack/bin/webpack.js",
26 | "--mode",
27 | "development",
28 | "--watch-stdin",
29 | cd: Path.expand("../assets", __DIR__)
30 | ]
31 | ]
32 |
33 | # ## SSL Support
34 | #
35 | # In order to use HTTPS in development, a self-signed
36 | # certificate can be generated by running the following
37 | # Mix task:
38 | #
39 | # mix phx.gen.cert
40 | #
41 | # Note that this task requires Erlang/OTP 20 or later.
42 | # Run `mix help phx.gen.cert` for more information.
43 | #
44 | # The `http:` config above can be replaced with:
45 | #
46 | # https: [
47 | # port: 4001,
48 | # cipher_suite: :strong,
49 | # keyfile: "priv/cert/selfsigned_key.pem",
50 | # certfile: "priv/cert/selfsigned.pem"
51 | # ],
52 | #
53 | # If desired, both `http:` and `https:` keys can be
54 | # configured to run both http and https servers on
55 | # different ports.
56 |
57 | # Watch static and templates for browser reloading.
58 | config :my_cool_app, MyCoolAppWeb.Endpoint,
59 | live_reload: [
60 | patterns: [
61 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
62 | ~r"priv/gettext/.*(po)$",
63 | ~r"lib/my_cool_app_web/{live,views}/.*(ex)$",
64 | ~r"lib/my_cool_app_web/templates/.*(eex)$"
65 | ]
66 | ]
67 |
68 | # Do not include metadata nor timestamps in development logs
69 | config :logger, :console, format: "[$level] $message\n"
70 |
71 | # Set a higher stacktrace during development. Avoid configuring such
72 | # in production as building large stacktraces may be expensive.
73 | config :phoenix, :stacktrace_depth, 20
74 |
75 | # Initialize plugs at runtime for faster development compilation
76 | config :phoenix, :plug_init_mode, :runtime
77 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, don't forget to configure the url host
4 | # to something meaningful, Phoenix uses this information
5 | # when generating URLs.
6 | #
7 | # Note we also include the path to a cache manifest
8 | # containing the digested version of static files. This
9 | # manifest is generated by the `mix phx.digest` task,
10 | # which you should run after static files are built and
11 | # before starting your production server.
12 | config :my_cool_app, MyCoolAppWeb.Endpoint,
13 | url: [host: "example.com", port: 80],
14 | cache_static_manifest: "priv/static/cache_manifest.json"
15 |
16 | # Do not print debug messages in production
17 | config :logger, level: :info
18 |
19 | # ## SSL Support
20 | #
21 | # To get SSL working, you will need to add the `https` key
22 | # to the previous section and set your `:url` port to 443:
23 | #
24 | # config :my_cool_app, MyCoolAppWeb.Endpoint,
25 | # ...
26 | # url: [host: "example.com", port: 443],
27 | # https: [
28 | # :inet6,
29 | # port: 443,
30 | # cipher_suite: :strong,
31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
33 | # ]
34 | #
35 | # The `cipher_suite` is set to `:strong` to support only the
36 | # latest and more secure SSL ciphers. This means old browsers
37 | # and clients may not be supported. You can set it to
38 | # `:compatible` for wider support.
39 | #
40 | # `:keyfile` and `:certfile` expect an absolute path to the key
41 | # and cert in disk or a relative path inside priv, for example
42 | # "priv/ssl/server.key". For all supported SSL configuration
43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
44 | #
45 | # We also recommend setting `force_ssl` in your endpoint, ensuring
46 | # no data is ever sent via http, always redirecting to https:
47 | #
48 | # config :my_cool_app, MyCoolAppWeb.Endpoint,
49 | # force_ssl: [hsts: true]
50 | #
51 | # Check `Plug.SSL` for all available options in `force_ssl`.
52 |
53 | # ## Using releases (Elixir v1.9+)
54 | #
55 | # If you are doing OTP releases, you need to instruct Phoenix
56 | # to start each relevant endpoint:
57 | #
58 | # config :my_cool_app, MyCoolAppWeb.Endpoint, server: true
59 | #
60 | # Then you can assemble a release by calling `mix release`.
61 | # See `mix help release` for more information.
62 |
63 | # Finally import the config/prod.secret.exs which loads secrets
64 | # and configuration from environment variables.
65 | import_config "prod.secret.exs"
66 |
--------------------------------------------------------------------------------
/config/prod.secret.exs:
--------------------------------------------------------------------------------
1 | # In this file, we load production configuration and
2 | # secrets from environment variables. You can also
3 | # hardcode secrets, although such is generally not
4 | # recommended and you have to remember to add this
5 | # file to your .gitignore.
6 | use Mix.Config
7 |
8 | database_url =
9 | System.get_env("DATABASE_URL") ||
10 | raise """
11 | environment variable DATABASE_URL is missing.
12 | For example: ecto://USER:PASS@HOST/DATABASE
13 | """
14 |
15 | config :my_cool_app, MyCoolApp.Repo,
16 | # ssl: true,
17 | url: database_url,
18 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
19 |
20 | secret_key_base =
21 | System.get_env("SECRET_KEY_BASE") ||
22 | raise """
23 | environment variable SECRET_KEY_BASE is missing.
24 | You can generate one by calling: mix phx.gen.secret
25 | """
26 |
27 | config :my_cool_app, MyCoolAppWeb.Endpoint,
28 | http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")],
29 | secret_key_base: secret_key_base
30 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure your database
4 | config :my_cool_app, MyCoolApp.Repo,
5 | username: "postgres",
6 | password: "postgres",
7 | database: "my_cool_app_test",
8 | hostname: "localhost",
9 | pool: Ecto.Adapters.SQL.Sandbox
10 |
11 | # We don't run a server during test. If one is required,
12 | # you can enable the server option below.
13 | config :my_cool_app, MyCoolAppWeb.Endpoint,
14 | http: [port: 4002],
15 | server: false
16 |
17 | # Print only warnings and errors during test
18 | config :logger, level: :warn
19 |
--------------------------------------------------------------------------------
/lib/my_cool_app.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp do
2 | @moduledoc """
3 | MyCoolApp keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/lib/my_cool_app/accounts.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp.Accounts do
2 | @moduledoc """
3 | The Accounts context.
4 | """
5 |
6 | import Ecto.Query, warn: false
7 | alias MyCoolApp.Repo
8 |
9 | alias MyCoolApp.Accounts.User
10 |
11 | @doc """
12 | Returns the list of users.
13 |
14 | ## Examples
15 |
16 | iex> list_users()
17 | [%User{}, ...]
18 |
19 | """
20 | def list_users do
21 | Repo.all(User)
22 | end
23 |
24 | @doc """
25 | Gets a single user.
26 |
27 | Raises `Ecto.NoResultsError` if the User does not exist.
28 |
29 | ## Examples
30 |
31 | iex> get_user!(123)
32 | %User{}
33 |
34 | iex> get_user!(456)
35 | ** (Ecto.NoResultsError)
36 |
37 | """
38 | def get_user!(id), do: Repo.get!(User, id)
39 |
40 | @doc """
41 | Creates a user.
42 |
43 | ## Examples
44 |
45 | iex> create_user(%{field: value})
46 | {:ok, %User{}}
47 |
48 | iex> create_user(%{field: bad_value})
49 | {:error, %Ecto.Changeset{}}
50 |
51 | """
52 | def create_user(attrs \\ %{}) do
53 | %User{}
54 | |> User.changeset(attrs)
55 | |> Repo.insert()
56 | end
57 |
58 | @doc """
59 | Updates a user.
60 |
61 | ## Examples
62 |
63 | iex> update_user(user, %{field: new_value})
64 | {:ok, %User{}}
65 |
66 | iex> update_user(user, %{field: bad_value})
67 | {:error, %Ecto.Changeset{}}
68 |
69 | """
70 | def update_user(%User{} = user, attrs) do
71 | user
72 | |> User.changeset(attrs)
73 | |> Repo.update()
74 | end
75 |
76 | @doc """
77 | Deletes a User.
78 |
79 | ## Examples
80 |
81 | iex> delete_user(user)
82 | {:ok, %User{}}
83 |
84 | iex> delete_user(user)
85 | {:error, %Ecto.Changeset{}}
86 |
87 | """
88 | def delete_user(%User{} = user) do
89 | Repo.delete(user)
90 | end
91 |
92 | @doc """
93 | Returns an `%Ecto.Changeset{}` for tracking user changes.
94 |
95 | ## Examples
96 |
97 | iex> change_user(user)
98 | %Ecto.Changeset{source: %User{}}
99 |
100 | """
101 | def change_user(%User{} = user) do
102 | User.changeset(user, %{})
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/lib/my_cool_app/accounts/user.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp.Accounts.User do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "users" do
6 | field :name, :string
7 | field :email, :string
8 | field :avatar_url, :string
9 |
10 | timestamps()
11 | end
12 |
13 | @required_fields ~w(name)a
14 | @optional_fields ~w(email avatar_url)a
15 | def changeset(user, attrs) do
16 | user
17 | |> cast(attrs, @required_fields ++ @optional_fields)
18 | |> validate_required(@required_fields)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/my_cool_app/application.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | def start(_type, _args) do
9 | import Supervisor.Spec
10 | # List all child processes to be supervised
11 | children = [
12 | # Start the Ecto repository
13 | MyCoolApp.Repo,
14 | # Start the endpoint when the application starts
15 | MyCoolAppWeb.Endpoint,
16 | # Starts a worker by calling: MyCoolApp.Worker.start_link(arg)
17 | # {MyCoolApp.Worker, arg},
18 | supervisor(Absinthe.Subscription, [MyCoolAppWeb.Endpoint])
19 | ]
20 |
21 | # See https://hexdocs.pm/elixir/Supervisor.html
22 | # for other strategies and supported options
23 | opts = [strategy: :one_for_one, name: MyCoolApp.Supervisor]
24 | Supervisor.start_link(children, opts)
25 | end
26 |
27 | # Tell Phoenix to update the endpoint configuration
28 | # whenever the application is updated.
29 | def config_change(changed, _new, removed) do
30 | MyCoolAppWeb.Endpoint.config_change(changed, removed)
31 | :ok
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/my_cool_app/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp.Repo do
2 | use Ecto.Repo,
3 | otp_app: :my_cool_app,
4 | adapter: Ecto.Adapters.Postgres
5 | end
6 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use MyCoolAppWeb, :controller
9 | use MyCoolAppWeb, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def controller do
21 | quote do
22 | use Phoenix.Controller, namespace: MyCoolAppWeb
23 |
24 | import Plug.Conn
25 | import MyCoolAppWeb.Gettext
26 | alias MyCoolAppWeb.Router.Helpers, as: Routes
27 | end
28 | end
29 |
30 | def view do
31 | quote do
32 | use Phoenix.View,
33 | root: "lib/my_cool_app_web/templates",
34 | namespace: MyCoolAppWeb
35 |
36 | # Import convenience functions from controllers
37 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
38 |
39 | # Use all HTML functionality (forms, tags, etc)
40 | use Phoenix.HTML
41 |
42 | import MyCoolAppWeb.ErrorHelpers
43 | import MyCoolAppWeb.Gettext
44 | alias MyCoolAppWeb.Router.Helpers, as: Routes
45 | end
46 | end
47 |
48 | def router do
49 | quote do
50 | use Phoenix.Router
51 | import Plug.Conn
52 | import Phoenix.Controller
53 | end
54 | end
55 |
56 | def channel do
57 | quote do
58 | use Phoenix.Channel
59 | import MyCoolAppWeb.Gettext
60 | end
61 | end
62 |
63 | @doc """
64 | When used, dispatch to the appropriate controller/view/etc.
65 | """
66 | defmacro __using__(which) when is_atom(which) do
67 | apply(__MODULE__, which, [])
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/channels/absinthe_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.AbsintheSocket do
2 | use Phoenix.Socket
3 | use Absinthe.Phoenix.Socket, schema: MyCoolAppWeb.Schema
4 |
5 | def connect(_, socket) do
6 | {:ok, socket}
7 | end
8 |
9 | def id(_), do: nil
10 | end
11 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | # channel "room:*", MyCoolAppWeb.RoomChannel
6 |
7 | # Socket params are passed from the client and can
8 | # be used to verify and authenticate a user. After
9 | # verification, you can put default assigns into
10 | # the socket that will be set for all channels, ie
11 | #
12 | # {:ok, assign(socket, :user_id, verified_user_id)}
13 | #
14 | # To deny connection, return `:error`.
15 | #
16 | # See `Phoenix.Token` documentation for examples in
17 | # performing token verification on connect.
18 | def connect(_params, socket, _connect_info) do
19 | {:ok, socket}
20 | end
21 |
22 | # Socket id's are topics that allow you to identify all sockets for a given user:
23 | #
24 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
25 | #
26 | # Would allow you to broadcast a "disconnect" event and terminate
27 | # all active sockets and channels for a given user:
28 | #
29 | # MyCoolAppWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
30 | #
31 | # Returning `nil` makes this socket anonymous.
32 | def id(_socket), do: nil
33 | end
34 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.PageController do
2 | use MyCoolAppWeb, :controller
3 |
4 | def index(conn, _params) do
5 | render(conn, "index.html")
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :my_cool_app
3 | use Absinthe.Phoenix.Endpoint
4 |
5 | socket "/socket", MyCoolAppWeb.AbsintheSocket,
6 | websocket: true,
7 | longpoll: false
8 |
9 | # Code reloading can be explicitly enabled under the
10 | # :code_reloader configuration of your endpoint.
11 | if code_reloading? do
12 | plug Phoenix.CodeReloader
13 | end
14 |
15 | plug Plug.RequestId
16 | plug Plug.Logger
17 |
18 | plug Plug.Parsers,
19 | parsers: [:urlencoded, :multipart, :json],
20 | pass: ["*/*"],
21 | json_decoder: Phoenix.json_library()
22 |
23 | plug CORSPlug
24 |
25 | plug MyCoolAppWeb.Router
26 | end
27 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.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 MyCoolAppWeb.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: :my_cool_app
24 | end
25 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/resolvers/account_resolver.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.Resolvers.AccountResolver do
2 | alias MyCoolApp.Accounts
3 |
4 | def list_users(_parent, _args, _resolutions) do
5 | {:ok, Accounts.list_users()}
6 | end
7 |
8 | def create_user(_parent, args, _resolutions) do
9 | args
10 | |> Accounts.create_user()
11 | |> case do
12 | {:ok, user} ->
13 | {:ok, user}
14 |
15 | {:error, changeset} ->
16 | {:error, extract_error_msg(changeset)}
17 | end
18 | end
19 |
20 | defp extract_error_msg(changeset) do
21 | changeset.errors
22 | |> Enum.map(fn {field, {error, _details}} ->
23 | [
24 | field: field,
25 | message: String.capitalize(error)
26 | ]
27 | end)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.Router do
2 | use MyCoolAppWeb, :router
3 |
4 | pipeline :api do
5 | plug :accepts, ["json"]
6 | end
7 |
8 | scope "/" do
9 | pipe_through :api
10 |
11 | forward "/graphiql", Absinthe.Plug.GraphiQL, schema: MyCoolAppWeb.Schema
12 |
13 | forward "/", Absinthe.Plug, schema: MyCoolAppWeb.Schema
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/schema.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.Schema do
2 | use Absinthe.Schema
3 |
4 | import_types(Absinthe.Type.Custom)
5 | import_types(MyCoolAppWeb.Schema.AccountTypes)
6 |
7 | query do
8 | import_fields(:account_queries)
9 | end
10 |
11 | mutation do
12 | import_fields(:account_mutations)
13 | end
14 |
15 | subscription do
16 | import_fields(:account_subscriptions)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/schema/account_types.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.Schema.AccountTypes do
2 | use Absinthe.Schema.Notation
3 | use Absinthe.Ecto, repo: MyCoolApp.Repo
4 |
5 | alias MyCoolAppWeb.Resolvers
6 |
7 | @desc "One user"
8 | object :user do
9 | field :id, :id
10 | field :name, :string
11 | field :email, :string
12 | field :avatar_url, :string
13 | end
14 |
15 | object :account_queries do
16 | @desc "Get all users"
17 | field :list_users, list_of(:user) do
18 | resolve(&Resolvers.AccountResolver.list_users/3)
19 | end
20 | end
21 |
22 | object :account_mutations do
23 | field :create_user, :user do
24 | arg(:name, non_null(:string))
25 | arg(:email, :string)
26 |
27 | resolve(&Resolvers.AccountResolver.create_user/3)
28 | end
29 | end
30 |
31 | object :account_subscriptions do
32 | field :user_created, :user do
33 | config(fn _, _ ->
34 | {:ok, topic: "users"}
35 | end)
36 |
37 | trigger(:create_user,
38 | topic: fn _ ->
39 | "users"
40 | end
41 | )
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | MyCoolApp · Phoenix Framework
8 | "/>
9 |
10 |
11 |
23 |
24 | <%= get_flash(@conn, :info) %>
25 | <%= get_flash(@conn, :error) %>
26 | <%= render @view_module, @view_template, assigns %>
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 | <%= gettext "Welcome to %{name}!", name: "Phoenix" %>
3 | A productive web framework that
does not compromise speed or maintainability.
4 |
5 |
6 |
7 |
8 | Resources
9 |
20 |
21 |
22 | Help
23 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn error ->
13 | content_tag(:span, translate_error(error), class: "help-block")
14 | end)
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # When using gettext, we typically pass the strings we want
22 | # to translate as a static argument:
23 | #
24 | # # Translate "is invalid" in the "errors" domain
25 | # dgettext("errors", "is invalid")
26 | #
27 | # # Translate the number of files with plural rules
28 | # dngettext("errors", "1 file", "%{count} files", count)
29 | #
30 | # Because the error messages we show in our forms and APIs
31 | # are defined inside Ecto, we need to translate them dynamically.
32 | # This requires us to call the Gettext module passing our gettext
33 | # backend as first argument.
34 | #
35 | # Note we use the "errors" domain, which means translations
36 | # should be written to the errors.po file. The :count option is
37 | # set by Ecto and indicates we should also apply plural rules.
38 | if count = opts[:count] do
39 | Gettext.dngettext(MyCoolAppWeb.Gettext, "errors", msg, msg, count, opts)
40 | else
41 | Gettext.dgettext(MyCoolAppWeb.Gettext, "errors", msg, opts)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.ErrorView do
2 | use MyCoolAppWeb, :view
3 |
4 | # If you want to customize a particular status code
5 | # for a certain format, you may uncomment below.
6 | # def render("500.html", _assigns) do
7 | # "Internal Server Error"
8 | # end
9 |
10 | # By default, Phoenix returns the status message from
11 | # the template name. For example, "404.html" becomes
12 | # "Not Found".
13 | def template_not_found(template, _assigns) do
14 | Phoenix.Controller.status_message_from_template(template)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.LayoutView do
2 | use MyCoolAppWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/my_cool_app_web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.PageView do
2 | use MyCoolAppWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :my_cool_app,
7 | version: "0.1.0",
8 | elixir: "~> 1.5",
9 | elixirc_paths: elixirc_paths(Mix.env()),
10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(),
11 | start_permanent: Mix.env() == :prod,
12 | aliases: aliases(),
13 | deps: deps()
14 | ]
15 | end
16 |
17 | # Configuration for the OTP application.
18 | #
19 | # Type `mix help compile.app` for more information.
20 | def application do
21 | [
22 | mod: {MyCoolApp.Application, []},
23 | extra_applications: [:logger, :runtime_tools]
24 | ]
25 | end
26 |
27 | # Specifies which paths to compile per environment.
28 | defp elixirc_paths(:test), do: ["lib", "test/support"]
29 | defp elixirc_paths(_), do: ["lib"]
30 |
31 | # Specifies your project dependencies.
32 | #
33 | # Type `mix help deps` for examples and options.
34 | defp deps do
35 | [
36 | {:absinthe, "~> 1.4"},
37 | {:absinthe_ecto, "~> 0.1.3"},
38 | {:absinthe_plug, "~> 1.4"},
39 | {:absinthe_phoenix, "~> 1.4.0"},
40 | {:cors_plug, "~> 2.0"},
41 | {:phoenix, "~> 1.4.4"},
42 | {:phoenix_pubsub, "~> 1.1"},
43 | {:phoenix_ecto, "~> 4.0"},
44 | {:ecto_sql, "~> 3.0"},
45 | {:postgrex, ">= 0.0.0"},
46 | {:phoenix_html, "~> 2.11"},
47 | {:phoenix_live_reload, "~> 1.2", only: :dev},
48 | {:gettext, "~> 0.11"},
49 | {:jason, "~> 1.0"},
50 | {:plug_cowboy, "~> 2.0"}
51 | ]
52 | end
53 |
54 | # Aliases are shortcuts or tasks specific to the current project.
55 | # For example, to create, migrate and run the seeds file at once:
56 | #
57 | # $ mix ecto.setup
58 | #
59 | # See the documentation for `Mix` for more info on aliases.
60 | defp aliases do
61 | [
62 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
63 | "ecto.reset": ["ecto.drop", "ecto.setup"],
64 | test: ["ecto.create --quiet", "ecto.migrate", "test"]
65 | ]
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "absinthe": {:hex, :absinthe, "1.4.16", "0933e4d9f12652b12115d5709c0293a1bf78a22578032e9ad0dad4efee6b9eb1", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
3 | "absinthe_ecto": {:hex, :absinthe_ecto, "0.1.3", "420b68129e79fe4571a4838904ba03e282330d335da47729ad52ffd7b8c5fcb1", [:mix], [{:absinthe, "~> 1.3.0 or ~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm"},
4 | "absinthe_phoenix": {:hex, :absinthe_phoenix, "1.4.4", "af3b7b44483121f756ea0ec75a536b74f67cdd62ec6a34b9e58df1fb4662389e", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4.0", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm"},
5 | "absinthe_plug": {:hex, :absinthe_plug, "1.4.7", "939b6b9e1c7abc6b399a5b49faa690a1fbb55b195c670aa35783b14b08ccec7a", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
6 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
7 | "cors_plug": {:hex, :cors_plug, "2.0.0", "238ddb479f92b38f6dc1ae44b8d81f0387f9519101a6da442d543ab70ee0e482", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
8 | "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
9 | "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"},
10 | "db_connection": {:hex, :db_connection, "2.1.0", "122e2f62c4906bf2e49554f1e64db5030c19229aa40935f33088e7d543aa79d0", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
11 | "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
12 | "ecto": {:hex, :ecto, "3.1.7", "fa21d06ef56cdc2fdaa62574e8c3ba34a2751d44ea34c30bc65f0728421043e5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
13 | "ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
14 | "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
15 | "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"},
16 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
17 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
18 | "phoenix": {:hex, :phoenix, "1.4.9", "746d098e10741c334d88143d3c94cab1756435f94387a63441792e66ec0ee974", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
19 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
20 | "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
21 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
22 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
23 | "plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
24 | "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
25 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
26 | "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
27 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
28 | "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
29 | }
30 |
--------------------------------------------------------------------------------
/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_acceptance/3
26 | msgid "must be accepted"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_format/3
30 | msgid "has invalid format"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_subset/3
34 | msgid "has an invalid entry"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_exclusion/3
38 | msgid "is reserved"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.validate_confirmation/3
42 | msgid "does not match confirmation"
43 | msgstr ""
44 |
45 | ## From Ecto.Changeset.no_assoc_constraint/3
46 | msgid "is still associated with this entry"
47 | msgstr ""
48 |
49 | msgid "are still associated with this entry"
50 | msgstr ""
51 |
52 | ## From Ecto.Changeset.validate_length/3
53 | msgid "should be %{count} character(s)"
54 | msgid_plural "should be %{count} character(s)"
55 | msgstr[0] ""
56 | msgstr[1] ""
57 |
58 | msgid "should have %{count} item(s)"
59 | msgid_plural "should have %{count} item(s)"
60 | msgstr[0] ""
61 | msgstr[1] ""
62 |
63 | msgid "should be at least %{count} character(s)"
64 | msgid_plural "should be at least %{count} character(s)"
65 | msgstr[0] ""
66 | msgstr[1] ""
67 |
68 | msgid "should have at least %{count} item(s)"
69 | msgid_plural "should have at least %{count} item(s)"
70 | msgstr[0] ""
71 | msgstr[1] ""
72 |
73 | msgid "should be at most %{count} character(s)"
74 | msgid_plural "should be at most %{count} character(s)"
75 | msgstr[0] ""
76 | msgstr[1] ""
77 |
78 | msgid "should have at most %{count} item(s)"
79 | msgid_plural "should have at most %{count} item(s)"
80 | msgstr[0] ""
81 | msgstr[1] ""
82 |
83 | ## From Ecto.Changeset.validate_number/3
84 | msgid "must be less than %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than %{number}"
88 | msgstr ""
89 |
90 | msgid "must be less than or equal to %{number}"
91 | msgstr ""
92 |
93 | msgid "must be greater than or equal to %{number}"
94 | msgstr ""
95 |
96 | msgid "must be equal to %{number}"
97 | msgstr ""
98 |
--------------------------------------------------------------------------------
/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This 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 has 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_acceptance/3
24 | msgid "must be accepted"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_format/3
28 | msgid "has invalid format"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_subset/3
32 | msgid "has an invalid entry"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_exclusion/3
36 | msgid "is reserved"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.validate_confirmation/3
40 | msgid "does not match confirmation"
41 | msgstr ""
42 |
43 | ## From Ecto.Changeset.no_assoc_constraint/3
44 | msgid "is still associated with this entry"
45 | msgstr ""
46 |
47 | msgid "are still associated with this entry"
48 | msgstr ""
49 |
50 | ## From Ecto.Changeset.validate_length/3
51 | msgid "should be %{count} character(s)"
52 | msgid_plural "should be %{count} character(s)"
53 | msgstr[0] ""
54 | msgstr[1] ""
55 |
56 | msgid "should have %{count} item(s)"
57 | msgid_plural "should have %{count} item(s)"
58 | msgstr[0] ""
59 | msgstr[1] ""
60 |
61 | msgid "should be at least %{count} character(s)"
62 | msgid_plural "should be at least %{count} character(s)"
63 | msgstr[0] ""
64 | msgstr[1] ""
65 |
66 | msgid "should have at least %{count} item(s)"
67 | msgid_plural "should have at least %{count} item(s)"
68 | msgstr[0] ""
69 | msgstr[1] ""
70 |
71 | msgid "should be at most %{count} character(s)"
72 | msgid_plural "should be at most %{count} character(s)"
73 | msgstr[0] ""
74 | msgstr[1] ""
75 |
76 | msgid "should have at most %{count} item(s)"
77 | msgid_plural "should have at most %{count} item(s)"
78 | msgstr[0] ""
79 | msgstr[1] ""
80 |
81 | ## From Ecto.Changeset.validate_number/3
82 | msgid "must be less than %{number}"
83 | msgstr ""
84 |
85 | msgid "must be greater than %{number}"
86 | msgstr ""
87 |
88 | msgid "must be less than or equal to %{number}"
89 | msgstr ""
90 |
91 | msgid "must be greater than or equal to %{number}"
92 | msgstr ""
93 |
94 | msgid "must be equal to %{number}"
95 | msgstr ""
96 |
--------------------------------------------------------------------------------
/priv/repo/migrations/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto_sql],
3 | inputs: ["*.exs"]
4 | ]
5 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190705201815_create_users.exs:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp.Repo.Migrations.CreateUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:users) do
6 | add :name, :string, null: false
7 | add :email, :string
8 | add :avatar_url, :string
9 |
10 | timestamps()
11 | end
12 |
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/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 | # MyCoolApp.Repo.insert!(%MyCoolApp.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/test/my_cool_app/accounts_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp.AccountsTest do
2 | use MyCoolApp.DataCase
3 |
4 | alias MyCoolApp.Accounts
5 |
6 | describe "users" do
7 | alias MyCoolApp.Accounts.User
8 |
9 | @valid_attrs %{avatar_url: "some avatar_url", email: "some email", name: "some name"}
10 | @update_attrs %{avatar_url: "some updated avatar_url", email: "some updated email", name: "some updated name"}
11 | @invalid_attrs %{avatar_url: nil, email: nil, name: nil}
12 |
13 | def user_fixture(attrs \\ %{}) do
14 | {:ok, user} =
15 | attrs
16 | |> Enum.into(@valid_attrs)
17 | |> Accounts.create_user()
18 |
19 | user
20 | end
21 |
22 | test "list_users/0 returns all users" do
23 | user = user_fixture()
24 | assert Accounts.list_users() == [user]
25 | end
26 |
27 | test "get_user!/1 returns the user with given id" do
28 | user = user_fixture()
29 | assert Accounts.get_user!(user.id) == user
30 | end
31 |
32 | test "create_user/1 with valid data creates a user" do
33 | assert {:ok, %User{} = user} = Accounts.create_user(@valid_attrs)
34 | assert user.avatar_url == "some avatar_url"
35 | assert user.email == "some email"
36 | assert user.name == "some name"
37 | end
38 |
39 | test "create_user/1 with invalid data returns error changeset" do
40 | assert {:error, %Ecto.Changeset{}} = Accounts.create_user(@invalid_attrs)
41 | end
42 |
43 | test "update_user/2 with valid data updates the user" do
44 | user = user_fixture()
45 | assert {:ok, %User{} = user} = Accounts.update_user(user, @update_attrs)
46 | assert user.avatar_url == "some updated avatar_url"
47 | assert user.email == "some updated email"
48 | assert user.name == "some updated name"
49 | end
50 |
51 | test "update_user/2 with invalid data returns error changeset" do
52 | user = user_fixture()
53 | assert {:error, %Ecto.Changeset{}} = Accounts.update_user(user, @invalid_attrs)
54 | assert user == Accounts.get_user!(user.id)
55 | end
56 |
57 | test "delete_user/1 deletes the user" do
58 | user = user_fixture()
59 | assert {:ok, %User{}} = Accounts.delete_user(user)
60 | assert_raise Ecto.NoResultsError, fn -> Accounts.get_user!(user.id) end
61 | end
62 |
63 | test "change_user/1 returns a user changeset" do
64 | user = user_fixture()
65 | assert %Ecto.Changeset{} = Accounts.change_user(user)
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/test/my_cool_app_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.PageControllerTest do
2 | use MyCoolAppWeb.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 |
--------------------------------------------------------------------------------
/test/my_cool_app_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.ErrorViewTest do
2 | use MyCoolAppWeb.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(MyCoolAppWeb.ErrorView, "404.html", []) == "Not Found"
9 | end
10 |
11 | test "renders 500.html" do
12 | assert render_to_string(MyCoolAppWeb.ErrorView, "500.html", []) == "Internal Server Error"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/my_cool_app_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.LayoutViewTest do
2 | use MyCoolAppWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/test/my_cool_app_web/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.PageViewTest do
2 | use MyCoolAppWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.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 common data structures and query the data layer.
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 | # The default endpoint for testing
24 | @endpoint MyCoolAppWeb.Endpoint
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyCoolApp.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(MyCoolApp.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolAppWeb.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 common data structures and query the data layer.
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 | alias MyCoolAppWeb.Router.Helpers, as: Routes
23 |
24 | # The default endpoint for testing
25 | @endpoint MyCoolAppWeb.Endpoint
26 | end
27 | end
28 |
29 | setup tags do
30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyCoolApp.Repo)
31 |
32 | unless tags[:async] do
33 | Ecto.Adapters.SQL.Sandbox.mode(MyCoolApp.Repo, {:shared, self()})
34 | end
35 |
36 | {:ok, conn: Phoenix.ConnTest.build_conn()}
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule MyCoolApp.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
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 MyCoolApp.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import MyCoolApp.DataCase
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyCoolApp.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(MyCoolApp.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 |
38 | @doc """
39 | A helper that transforms changeset errors into a map of messages.
40 |
41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
42 | assert "password is too short" in errors_on(changeset).password
43 | assert %{password: ["password is too short"]} = errors_on(changeset)
44 |
45 | """
46 | def errors_on(changeset) do
47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
48 | Enum.reduce(opts, message, fn {key, value}, acc ->
49 | String.replace(acc, "%{#{key}}", to_string(value))
50 | end)
51 | end)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 | Ecto.Adapters.SQL.Sandbox.mode(MyCoolApp.Repo, :manual)
3 |
--------------------------------------------------------------------------------