├── .formatter.exs ├── .gitignore ├── .travis.yml ├── Procfile ├── README.md ├── config ├── .credo.exs ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── elixir_buildpack.config ├── lib ├── postgres_pubub.ex └── postgres_pubub │ ├── account.ex │ ├── application.ex │ ├── listener.ex │ └── repo.ex ├── mix.exs ├── mix.lock ├── priv └── repo │ └── migrations │ ├── 20180908224448_create_notification_function_for_accounts.exs │ ├── 20180908224629_ensure_table_accounts.exs │ └── 20180910162226_create_trigger_for_accounts.exs ├── scripts └── lint.sh └── test ├── postgres_pubub_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "{mix,.formatter}.exs", 4 | "{config,lib,test}/**/*.{ex,exs}" 5 | ] 6 | ] 7 | -------------------------------------------------------------------------------- /.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 | postgres_pubub-*.tar 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 1.7 3 | otp_release: 21.0 4 | sudo: false 5 | script: 6 | - ./scripts/lint.sh 7 | - mix test 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: POOL_SIZE=1 mix ecto.migrate 2 | web: mix run --no-halt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postgres-pubub 2 | 3 | [![Build Status](https://travis-ci.org/KamilLelonek/postgres-pubsub-elixir.svg?branch=master)](https://travis-ci.org/KamilLelonek/postgres-pubsub-elixir) 4 | 5 | The purpose of this repository is to provide the complete implementation for the following article: 6 | 7 | https://k.lelonek.me/postgres-pubsub-elixir 8 | -------------------------------------------------------------------------------- /config/.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/**", "test/**"], 7 | excluded: [] 8 | }, 9 | checks: [ 10 | {Credo.Check.Readability.MaxLineLength, false}, 11 | {Credo.Check.Readability.ModuleDoc, false}, 12 | {Credo.Check.Readability.AliasOrder, false} 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :postgres_pubub, 4 | ecto_repos: [PostgresPubSub.Repo] 5 | 6 | config :postgres_pubub, PostgresPubSub.Repo, 7 | adapter: Ecto.Adapters.Postgres, 8 | hostname: "localhost", 9 | username: "postgres", 10 | password: "postgres", 11 | migration_timestamps: [type: :utc_datetime], 12 | migration_primary_key: [id: :uuid, type: :binary_id] 13 | 14 | import_config "#{Mix.env()}.exs" 15 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :postgres_pubub, PostgresPubSub.Repo, database: "postgres_pubub_dev" 4 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :postgres_pubub, PostgresPubSub.Repo, 4 | url: System.get_env("DATABASE_URL"), 5 | pool_size: String.to_integer(System.get_env("POOL_SIZE")), 6 | ssl: true 7 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :postgres_pubub, PostgresPubSub.Repo, 4 | database: "postgres_pubub_test", 5 | pool: Ecto.Adapters.SQL.Sandbox 6 | -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | erlang_version=21.0 2 | elixir_version=1.7.2 3 | -------------------------------------------------------------------------------- /lib/postgres_pubub.ex: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub do 2 | end 3 | -------------------------------------------------------------------------------- /lib/postgres_pubub/account.ex: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub.Account do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :binary_id, autogenerate: true} 5 | 6 | schema "accounts" do 7 | field(:username, :string) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/postgres_pubub/application.ex: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub.Application do 2 | use Application 3 | 4 | alias PostgresPubSub.{Repo, Listener} 5 | 6 | def start(_type, _args), 7 | do: Supervisor.start_link(children(), opts()) 8 | 9 | defp children do 10 | [ 11 | Repo, 12 | Listener 13 | ] 14 | end 15 | 16 | defp opts do 17 | [ 18 | strategy: :one_for_one, 19 | name: PostgresPubSub.Supervisor 20 | ] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/postgres_pubub/listener.ex: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub.Listener do 2 | use GenServer 3 | 4 | alias PostgresPubSub.Repo 5 | 6 | require Logger 7 | 8 | @event_name "accounts_changed" 9 | 10 | def child_spec(opts) do 11 | %{ 12 | id: __MODULE__, 13 | start: {__MODULE__, :start_link, [opts]} 14 | } 15 | end 16 | 17 | def start_link(opts \\ []), 18 | do: GenServer.start_link(__MODULE__, opts) 19 | 20 | def init(opts) do 21 | with {:ok, _pid, _ref} <- Repo.listen(@event_name) do 22 | {:ok, opts} 23 | else 24 | error -> {:stop, error} 25 | end 26 | end 27 | 28 | def handle_info({:notification, _pid, _ref, @event_name, payload}, _state) do 29 | with {:ok, data} <- Poison.decode(payload, keys: :atoms) do 30 | data 31 | |> inspect() 32 | |> Logger.info() 33 | 34 | {:noreply, :event_handled} 35 | else 36 | error -> {:stop, error, []} 37 | end 38 | end 39 | 40 | def handle_info(_, _state), do: {:noreply, :event_received} 41 | end 42 | -------------------------------------------------------------------------------- /lib/postgres_pubub/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub.Repo do 2 | use Ecto.Repo, 3 | otp_app: :postgres_pubub 4 | 5 | alias Postgrex.Notifications 6 | 7 | def child_spec(opts) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, [opts]}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def init(_, opts), do: {:ok, opts} 16 | 17 | def listen(event_name) do 18 | with {:ok, pid} <- Notifications.start_link(__MODULE__.config()), 19 | {:ok, ref} <- Notifications.listen(pid, event_name) do 20 | {:ok, pid, ref} 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :postgres_pubub, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | aliases: aliases() 12 | ] 13 | end 14 | 15 | def application do 16 | [ 17 | extra_applications: [:logger], 18 | mod: {PostgresPubSub.Application, []} 19 | ] 20 | end 21 | 22 | defp deps do 23 | [ 24 | {:postgrex, "~> 0.13.5"}, 25 | {:ecto, "~> 2.2"}, 26 | {:poison, "~> 3.0"}, 27 | {:credo, "~> 0.10", except: :prod, runtime: false} 28 | ] 29 | end 30 | 31 | defp aliases do 32 | [ 33 | "ecto.setup": ["ecto.create", "ecto.migrate"], 34 | "ecto.reset": ["ecto.drop", "ecto.setup"], 35 | test: ["ecto.reset", "test"] 36 | ] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 4 | "credo": {:hex, :credo, "0.10.1", "e85efaf2dd7054399083ab2c6a5199f6cb1805de1a5b00d9e8c5f07033407b1f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 6 | "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, 7 | "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 8 | "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 9 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 10 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, 11 | "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, 12 | } 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180908224448_create_notification_function_for_accounts.exs: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub.Repo.Migrations.CreateNotificationFunctionForAccounts do 2 | use Ecto.Migration 3 | 4 | @table_name "accounts" 5 | @function_name "notify_account_changes" 6 | @event_name "accounts_changed" 7 | 8 | def up do 9 | execute(""" 10 | CREATE OR REPLACE FUNCTION #{@function_name}() 11 | RETURNS trigger AS $$ 12 | BEGIN 13 | PERFORM pg_notify( 14 | '#{@event_name}', 15 | json_build_object( 16 | 'operation', TG_OP, 17 | 'record', row_to_json(NEW) 18 | )::text 19 | ); 20 | 21 | RETURN NEW; 22 | END; 23 | $$ LANGUAGE plpgsql; 24 | """) 25 | end 26 | 27 | def down do 28 | execute("DROP FUNCTION IF EXISTS #{@function_name} CASCADE") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180908224629_ensure_table_accounts.exs: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub.Repo.Migrations.EnsureTableAccounts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create_if_not_exists table(:accounts) do 6 | add(:username, :string, null: true) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180910162226_create_trigger_for_accounts.exs: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSub.Repo.Migrations.CreateTriggerForAccounts do 2 | use Ecto.Migration 3 | 4 | @table_name "accounts" 5 | @function_name "notify_account_changes" 6 | @trigger_name "accounts_changed" 7 | 8 | def change do 9 | execute("DROP TRIGGER IF EXISTS #{@trigger_name} ON #{@table_name}") 10 | 11 | execute(""" 12 | CREATE TRIGGER #{@trigger_name} 13 | AFTER INSERT OR UPDATE 14 | ON #{@table_name} 15 | FOR EACH ROW 16 | EXECUTE PROCEDURE #{@function_name}() 17 | """) 18 | end 19 | 20 | def down do 21 | execute("DROP TRIGGER IF EXISTS #{@trigger_name} ON #{@table_name}") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | mix credo --strict 5 | mix format --check-formatted 6 | -------------------------------------------------------------------------------- /test/postgres_pubub_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PostgresPubSubTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------