├── priv └── repo │ ├── seeds_demo.exs │ ├── seeds │ ├── post.exs │ ├── user.exs │ └── comment.exs │ ├── with_nonexisting_deps │ └── user.exs │ └── migrations │ └── 20220313154938_add_users_posts_and_comments_for_demo.exs ├── .tool-versions ├── test ├── support │ ├── seeder.ex │ ├── repo.ex │ ├── post.ex │ ├── comment.ex │ └── user.ex ├── test_helper.exs ├── utils_test.exs ├── drill_test.exs └── tasks │ └── drill_test.exs ├── .formatter.exs ├── lib ├── drill │ ├── application.ex │ ├── seed.ex │ ├── context.ex │ ├── utils.ex │ └── seeder.ex ├── tasks │ └── drill.ex └── drill.ex ├── .github ├── dependabot.yml └── workflows │ └── elixir.yml ├── config ├── config.exs └── test.exs ├── .gitignore ├── LICENSE ├── mix.exs ├── CHANGELOG.md ├── README.md └── mix.lock /priv/repo/seeds_demo.exs: -------------------------------------------------------------------------------- 1 | # Runs the demo seed 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.15.7-otp-26 2 | erlang 26.2 3 | -------------------------------------------------------------------------------- /test/support/seeder.ex: -------------------------------------------------------------------------------- 1 | defmodule SeederDemo do 2 | @moduledoc false 3 | end 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(seed: 0) 2 | ExUnit.start() 3 | Drill.Test.Repo.start_link() 4 | Ecto.Adapters.SQL.Sandbox.mode(Drill.Test.Repo, :manual) 5 | -------------------------------------------------------------------------------- /test/support/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.Repo do 2 | @moduledoc false 3 | 4 | use Ecto.Repo, 5 | otp_app: :drill, 6 | adapter: Ecto.Adapters.Postgres 7 | end 8 | -------------------------------------------------------------------------------- /test/support/post.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.Post do 2 | @moduledoc false 3 | use Ecto.Schema 4 | 5 | schema "posts" do 6 | field(:content, :string) 7 | belongs_to(:user, Drill.Test.User) 8 | 9 | timestamps() 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/drill/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | children = [] 7 | 8 | opts = [strategy: :one_for_one, name: Drill.Supervisor] 9 | Supervisor.start_link(children, opts) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/support/comment.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.Comment do 2 | @moduledoc false 3 | use Ecto.Schema 4 | 5 | schema "comments" do 6 | field(:content, :string) 7 | belongs_to(:user, Drill.Test.User) 8 | belongs_to(:post, Drill.Test.Post) 9 | 10 | timestamps() 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/support/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.User do 2 | @moduledoc """ 3 | Demo user 4 | """ 5 | use Ecto.Schema 6 | 7 | schema "users" do 8 | field(:email, :string) 9 | field(:first_name, :string) 10 | field(:last_name, :string) 11 | 12 | timestamps() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 99 8 | - package-ecosystem: mix 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | open-pull-requests-limit: 99 13 | -------------------------------------------------------------------------------- /lib/drill/seed.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Seed do 2 | @moduledoc "Contains helper and struct definition for seeds" 3 | @type t :: %__MODULE__{ 4 | attrs: map() 5 | } 6 | defstruct attrs: %{} 7 | 8 | def new(seeder, attrs \\ []) do 9 | input_attrs = Map.new(attrs) 10 | full_attrs = seeder.factory() |> Map.merge(input_attrs) 11 | %__MODULE__{attrs: full_attrs} 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/seeds/post.exs: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.PostSeed do 2 | @moduledoc false 3 | use Drill, key: :posts, source: Drill.Test.Post 4 | alias Faker.Lorem 5 | 6 | @impl true 7 | def deps do 8 | [Drill.Test.UserSeed] 9 | end 10 | 11 | @impl true 12 | def factory do 13 | %{content: Lorem.paragraph()} 14 | end 15 | 16 | @impl true 17 | def run(%Drill.Context{seeds: %{users: [user1, user2, user3 | _]}}) do 18 | for user <- [user1, user2, user3] do 19 | seed(user_id: user.id) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /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 | import Config 9 | 10 | # Import environment specific config. This must remain at the bottom 11 | # of this file so it overrides the configuration defined above. 12 | if File.exists?("config/#{Mix.env()}.exs") do 13 | import_config "#{Mix.env()}.exs" 14 | end 15 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Configure your database 4 | # 5 | # The MIX_TEST_PARTITION environment variable can be used 6 | # to provide built-in test partitioning in CI environment. 7 | # Run `mix help test` for more information. 8 | config :drill, Drill.Test.Repo, 9 | username: "postgres", 10 | password: "postgres", 11 | database: "drill_test#{System.get_env("MIX_TEST_PARTITION")}", 12 | hostname: "localhost", 13 | pool: Ecto.Adapters.SQL.Sandbox 14 | 15 | config :drill, ecto_repos: [Drill.Test.Repo] 16 | 17 | # Print only warnings and errors during test 18 | config :logger, level: :warning 19 | -------------------------------------------------------------------------------- /priv/repo/seeds/user.exs: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.UserSeed do 2 | @moduledoc false 3 | use Drill, key: :users, source: Drill.Test.User 4 | 5 | alias Faker.Person 6 | 7 | @impl true 8 | def constraints, do: [:email] 9 | 10 | @impl true 11 | def factory do 12 | %{ 13 | email: Faker.Internet.email(), 14 | first_name: Person.first_name(), 15 | last_name: Person.last_name() 16 | } 17 | end 18 | 19 | @impl true 20 | def run(%Drill.Context{}) do 21 | [ 22 | seed(email: "email1@example.com"), 23 | seed(email: "email2@example.com"), 24 | seed(email: "email3@example.com") 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /priv/repo/seeds/comment.exs: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.CommentSeed do 2 | @moduledoc false 3 | use Drill, key: :comment, source: Drill.Test.Comment 4 | alias Faker.Lorem 5 | 6 | @impl true 7 | def deps do 8 | [Drill.Test.PostSeed] 9 | end 10 | 11 | @impl true 12 | def factory do 13 | %{content: Lorem.paragraph()} 14 | end 15 | 16 | @impl true 17 | def run(%Drill.Context{ 18 | seeds: %{posts: [post1, post2, post3 | _], users: [user1, user2, user3 | _]} 19 | }) do 20 | [ 21 | seed(user_id: user1.id, post_id: post1.id), 22 | seed(user_id: user2.id, post_id: post2.id), 23 | seed(user_id: user3.id, post_id: post3.id) 24 | ] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/drill/context.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Context do 2 | @moduledoc """ 3 | Seed struct containing all inserted items as seeds 4 | """ 5 | @type t :: %__MODULE__{ 6 | seeds: %{atom() => any()}, 7 | seeders: [atom()], 8 | pending_seeders: [atom()], 9 | completed_seeders: [atom()], 10 | seed_count: %{atom() => integer() | {:for_each, atom(), keyword()}}, 11 | repo: atom(), 12 | prefix: String.t() | nil 13 | } 14 | 15 | defstruct seeds: %{}, 16 | seeders: [], 17 | pending_seeders: [], 18 | completed_seeders: [], 19 | seed_count: %{}, 20 | repo: nil, 21 | prefix: nil 22 | end 23 | -------------------------------------------------------------------------------- /.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 third-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 | drill-*.tar 24 | 25 | 26 | # Temporary files for e.g. tests 27 | /tmp 28 | -------------------------------------------------------------------------------- /priv/repo/with_nonexisting_deps/user.exs: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.NonExistingDeps.UserSeed do 2 | @moduledoc false 3 | use Drill, key: :users, source: Drill.Test.User 4 | 5 | alias Faker.Person 6 | 7 | @impl true 8 | def constraints, do: [:email] 9 | 10 | @impl true 11 | def deps, do: [Drill.Test.NonExistingDep] 12 | 13 | @impl true 14 | def factory do 15 | %{ 16 | email: Faker.Internet.email(), 17 | first_name: Person.first_name(), 18 | last_name: Person.last_name() 19 | } 20 | end 21 | 22 | @impl true 23 | def run(%Drill.Context{}) do 24 | [ 25 | seed(email: "email1@example.com"), 26 | seed(email: "email2@example.com"), 27 | seed(email: "email3@example.com") 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Seeder1 do 2 | def deps, do: [] 3 | end 4 | 5 | defmodule Seeder2 do 6 | def deps, do: [Seeder1] 7 | end 8 | 9 | defmodule Seeder3 do 10 | def deps, do: [Seeder1, Seeder2] 11 | end 12 | 13 | defmodule SeederWithUnmatchingDeps do 14 | def deps, do: [CannotMatchSeeder] 15 | end 16 | 17 | defmodule Drills.UtilsTest do 18 | use ExUnit.Case, async: true 19 | alias Drill.Utils 20 | 21 | test "sort_seeders_by_deps/1" do 22 | actual_result = 23 | Utils.sort_seeders_by_deps( 24 | Enum.shuffle([ 25 | Seeder1, 26 | Seeder2, 27 | Seeder3, 28 | SeederWithUnmatchingDeps 29 | ]) 30 | ) 31 | 32 | assert actual_result == [Seeder1, Seeder2, Seeder3] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220313154938_add_users_posts_and_comments_for_demo.exs: -------------------------------------------------------------------------------- 1 | defmodule Drill.Test.Repo.Migrations.AddUsersPostsAndCommentsForDemo do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add(:email, :string) 7 | add(:first_name, :string) 8 | add(:last_name, :string) 9 | timestamps() 10 | end 11 | 12 | create table(:posts) do 13 | add(:content, :text) 14 | add(:user_id, references(:users)) 15 | timestamps() 16 | end 17 | 18 | create table(:comments) do 19 | add(:content, :text) 20 | add(:user_id, references(:users)) 21 | add(:post_id, references(:posts)) 22 | timestamps() 23 | end 24 | 25 | create(unique_index(:users, [:email])) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/drill_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DrillTest do 2 | use ExUnit.Case 3 | doctest Drill 4 | 5 | contents = 6 | quote do 7 | use Drill, source: MySource, key: :my_key 8 | def run(_), do: [] 9 | end 10 | 11 | Module.create(Demo, contents, Macro.Env.location(__ENV__)) 12 | 13 | test "definitions" do 14 | assert Demo.__info__(:functions) == [ 15 | constraints: 0, 16 | context_key: 0, 17 | deps: 0, 18 | factory: 0, 19 | on_conflict: 0, 20 | returning: 0, 21 | run: 1, 22 | schema: 0, 23 | seed: 0, 24 | seed: 1 25 | ] 26 | end 27 | 28 | test "context_key/0 returns opts `:key`" do 29 | assert Demo.context_key() == :my_key 30 | end 31 | 32 | test "schema/0 returns opts `:source`" do 33 | assert Demo.schema() == MySource 34 | end 35 | 36 | test "deps/0 and constraints/0 returns empty list by default" do 37 | assert Demo.constraints() == [] 38 | assert Demo.deps() == [] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 dgigafox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/drill/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Utils do 2 | @moduledoc """ 3 | Utilities and helpers 4 | """ 5 | 6 | @doc """ 7 | Sort seeder modules by dependencies. Seeders without deps are put first in the list, 8 | next to seeders with deps already in the list, and so on... 9 | """ 10 | def sort_seeders_by_deps(seeders) do 11 | seeders_with_no_deps = Enum.filter(seeders, &Enum.empty?(&1.deps())) 12 | 13 | seeders_with_no_deps 14 | |> do_sort_seeders_by_deps(seeders -- seeders_with_no_deps) 15 | |> Enum.reverse() 16 | end 17 | 18 | defp do_sort_seeders_by_deps(arranged_seeders, remaining_seeders) do 19 | next_seeders = 20 | Enum.filter( 21 | remaining_seeders, 22 | &MapSet.subset?(MapSet.new(&1.deps()), MapSet.new(arranged_seeders)) 23 | ) 24 | 25 | arranged_seeders = List.flatten([next_seeders | arranged_seeders]) 26 | 27 | if Enum.empty?(next_seeders) do 28 | arranged_seeders 29 | else 30 | do_sort_seeders_by_deps(arranged_seeders, remaining_seeders -- next_seeders) 31 | end 32 | end 33 | 34 | @spec merge_autogenerated_fields_to_entries([map()], map()) :: [map()] 35 | def merge_autogenerated_fields_to_entries(entries, auto_generated_fields) do 36 | Enum.map(entries, &Map.merge(auto_generated_fields, &1)) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/drill/seeder.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill.Seeder do 2 | @moduledoc """ 3 | Module responsible for seeding data into the database 4 | """ 5 | alias Drill.Seed 6 | @spec list_seeder_modules(seeders_path :: binary()) :: [module()] 7 | def list_seeder_modules(seeders_path) do 8 | seeders_path 9 | |> list_seeder_files() 10 | |> compile_seeders() 11 | |> Enum.filter(&seeder?/1) 12 | end 13 | 14 | def seeders_path(repo, directory) do 15 | config = repo.config() 16 | priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" 17 | app = Keyword.fetch!(config, :otp_app) 18 | Application.app_dir(app, Path.join(priv, directory)) 19 | end 20 | 21 | defp list_seeder_files(seeders_source) do 22 | Path.join([seeders_source, "**", "*.exs"]) 23 | |> Path.wildcard() 24 | end 25 | 26 | defp compile_seeders(files) do 27 | files 28 | |> Enum.flat_map(fn file -> 29 | file 30 | |> Code.compile_file() 31 | |> Enum.map(&elem(&1, 0)) 32 | end) 33 | end 34 | 35 | defp seeder?(module) do 36 | Drill in (module.__info__(:attributes)[:behaviour] || []) 37 | end 38 | 39 | @spec build_entries_from_seeds(list(any())) :: list(map()) 40 | def build_entries_from_seeds(seeds) do 41 | seeds 42 | |> Enum.filter(&is_struct(&1, Seed)) 43 | |> Enum.map(& &1.attrs) 44 | end 45 | 46 | @spec filter_manual_seeds(list(any())) :: list(any()) 47 | def filter_manual_seeds(seeds) do 48 | seeds 49 | |> Enum.reject(&is_struct(&1, Seed)) 50 | end 51 | 52 | @spec autogenerate_fields(module() | binary()) :: map() 53 | def autogenerate_fields(schema) when is_atom(schema) do 54 | for {fields, {func, name, args}} <- schema.__schema__(:autogenerate), 55 | field <- fields, 56 | into: %{} do 57 | {field, apply(func, name, args)} 58 | end 59 | end 60 | 61 | def autogenerate_fields(_source), do: %{} 62 | end 63 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Drill.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/dgigafox/drill" 5 | 6 | def project do 7 | [ 8 | app: :drill, 9 | version: "1.2.3", 10 | elixir: "~> 1.13", 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps(), 14 | dialyzer: dialyzer(), 15 | package: package(), 16 | description: description(), 17 | elixirc_paths: elixirc_paths(Mix.env()) 18 | ] 19 | end 20 | 21 | # Run "mix help compile.app" to learn about applications. 22 | def application do 23 | [ 24 | mod: {Drill.Application, []}, 25 | extra_applications: [:logger] 26 | ] 27 | end 28 | 29 | # Run "mix help deps" to learn about dependencies. 30 | defp deps do 31 | [ 32 | {:ecto_sql, "~> 3.0"}, 33 | {:postgrex, ">= 0.0.0"}, 34 | {:faker, "~> 0.17"}, 35 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 36 | {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, 37 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 38 | ] 39 | end 40 | 41 | defp aliases do 42 | [ 43 | setup: ["deps.get", "ecto.setup"], 44 | "ecto.setup": ["ecto.create", "ecto.migrate"], 45 | "ecto.reset": ["ecto.drop", "ecto.setup"], 46 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] 47 | ] 48 | end 49 | 50 | defp dialyzer do 51 | [ 52 | plt_core_path: "priv/plts", 53 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, 54 | plt_add_apps: [:mix] 55 | ] 56 | end 57 | 58 | defp elixirc_paths(:test), do: ["lib", "test/support"] 59 | defp elixirc_paths(_), do: ["lib"] 60 | 61 | defp description do 62 | """ 63 | Seed data handling for Elixir 64 | """ 65 | end 66 | 67 | defp package do 68 | [ 69 | licenses: ["MIT"], 70 | links: %{"GitHub" => @source_url} 71 | ] 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/tasks/drill_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.DrillTest do 2 | use ExUnit.Case, async: true 3 | alias Drill.Test.Repo 4 | alias Ecto.Adapters.SQL.Sandbox 5 | alias Mix.Tasks.Drill, as: DrillTask 6 | 7 | test "Mix.Tasks.Drill is a task" do 8 | assert Mix.Task.task?(DrillTask) 9 | end 10 | 11 | test "Mix.Tasks.Drill task name is task" do 12 | assert Mix.Task.task_name(DrillTask) == "drill" 13 | end 14 | 15 | describe "run/1" do 16 | setup do 17 | Sandbox.checkout(Repo) 18 | :ok 19 | end 20 | 21 | test "seeds the database" do 22 | Mix.Task.run("drill", ["-r", "Drill.Test.Repo"]) 23 | 24 | assert [ 25 | %{id: user1_id, email: "email1@example.com"}, 26 | %{id: user2_id, email: "email2@example.com"}, 27 | %{id: user3_id, email: "email3@example.com"} 28 | ] = Drill.Test.User |> Repo.all() |> Enum.sort_by(& &1.email) 29 | 30 | assert [ 31 | %{id: post1_id, user_id: ^user1_id}, 32 | %{id: post2_id, user_id: ^user2_id}, 33 | %{id: post3_id, user_id: ^user3_id} 34 | ] = 35 | Drill.Test.Post 36 | |> Repo.all() 37 | |> Repo.preload(:user) 38 | |> Enum.sort_by(& &1.user.email) 39 | 40 | assert [ 41 | %{user_id: ^user1_id, post_id: ^post1_id}, 42 | %{user_id: ^user2_id, post_id: ^post2_id}, 43 | %{user_id: ^user3_id, post_id: ^post3_id} 44 | ] = 45 | Drill.Test.Comment 46 | |> Repo.all() 47 | |> Repo.preload(:user) 48 | |> Enum.sort_by(& &1.user.email) 49 | end 50 | 51 | test "raises an error if any seeder has nonexisting deps" do 52 | seeds_path = Path.join(:code.priv_dir(:drill), "repo/with_nonexisting_deps") 53 | 54 | assert_raise RuntimeError, fn -> 55 | Mix.Task.rerun("drill", ["-r", "Drill.Test.Repo", "--seeds-path", seeds_path]) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [1.2.3] - 2024-05-27 9 | 10 | ### Changed 11 | 12 | - Add parenthesis to invoked deps/0 function 13 | - Update dependencies 14 | 15 | ## [1.2.2] - 2023-08-08 16 | 17 | ### Changed 18 | 19 | - Remove warning when any seeder has nonexisting dependency 20 | 21 | ### Added 22 | 23 | - Raise and display error when any seeder has nonexisting dependency 24 | 25 | ### Fixed 26 | 27 | - Update `constraints/0` callback typespec 28 | 29 | ## [1.2.1] - 2023-07-20 30 | 31 | ### Added 32 | 33 | - Add dependabot 34 | 35 | ### Fixed 36 | 37 | - Fix `run/1` callback being called twice per seeder during seed task 38 | - Update github actions 39 | 40 | ## [1.2.0] - 2023-07-02 41 | 42 | ### Added 43 | 44 | - Add `:returning` option to `use Drill` 45 | - Allow user to set `:source` to table name instead of schema 46 | - Add `:repo` to `Drill.Context` 47 | - Allow user to override location of seeder file through `seeds_path` option when running `mix drill`. E.g. `mix drill -r MyApp.Repo --seeds-path priv/seeds/core` 48 | - Allow manual seeds and set callback `factory/0` to optional 49 | 50 | ### Fixed 51 | 52 | - Remove running migration when running task. Migration should now be run by explicitly before running `mix drill` 53 | - Start all registered apps before seeding to ensure apps required by seeds have started such as uploaders, genservers, dependencies, etc. 54 | 55 | ## [1.1.0] - 2023-06-16 56 | 57 | ### Added 58 | 59 | - Allow user to set `on_conflict` strategy using optional callback `on_conflict/0` 60 | - Require seeder files to be .exs and remove the need to config `:otp_app` 61 | - Allow user to set `timeout` through config. E.g. `config :drill, :timeout, :infinity` 62 | 63 | ## [1.0.0] - 2023-06-14 64 | 65 | ### Changed 66 | 67 | - Upgrade Elixir & Erlang tool versions to 1.14.5-otp-25 and 25.3.2, respectively 68 | 69 | ### Added 70 | 71 | - Add new required callback `factory/0` 72 | - Add new functions `seed/0` & `seed/1` that return `Drill.Seed` struct 73 | - Require `run/0` callback to always return a list of `Drill.Seed` struct 74 | 75 | ## [0.1.1] - 2022-05-28 76 | 77 | ### Fixed 78 | 79 | - Remove Drill.Demo.Repo from being run on dev 80 | - Rename module with `Demo` to `Test` (e.g. `Drill.Demo.Repo` is renamed to `Drill.Test.Repo`) 81 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: push 4 | 5 | env: 6 | MIX_ENV: test 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | services: 12 | postgres: 13 | image: postgres 14 | env: 15 | POSTGRES_USER: postgres 16 | POSTGRES_PASSWORD: postgres 17 | POSTGRES_DB: test_drill 18 | ports: 19 | - 5432:5432 20 | options: >- 21 | --health-cmd pg_isready 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | 26 | strategy: 27 | matrix: 28 | elixir: ["1.13", "1.14", "1.15"] 29 | otp: ["25", "26"] 30 | exclude: 31 | - elixir: 1.13 32 | otp: 26 33 | - elixir: 1.14 34 | otp: 26 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Setup elixir 40 | uses: erlef/setup-beam@v1 41 | with: 42 | elixir-version: ${{ matrix.elixir }} 43 | otp-version: ${{ matrix.otp }} 44 | 45 | - name: Restore dependencies cache 46 | uses: actions/cache@v4 47 | id: mix-cache 48 | with: 49 | path: deps 50 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 51 | restore-keys: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 52 | 53 | - name: Install dependencies 54 | if: steps.mix-cache.outputs.cache-hit != 'true' 55 | run: | 56 | mix local.rebar --force 57 | mix local.hex --force 58 | mix deps.get 59 | 60 | - name: Compile & error on warning 61 | run: mix compile --warnings-as-errors 62 | 63 | - name: Check formatting 64 | run: mix format --check-formatted 65 | 66 | - name: Run credo 67 | run: mix credo --strict 68 | 69 | - name: Retrieve PLT cache 70 | uses: actions/cache@v4 71 | id: plt-cache 72 | with: 73 | path: priv/plts 74 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 75 | restore-keys: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 76 | 77 | - name: Create PLTs 78 | if: steps.plt-cache.outputs.cache-hit != 'true' 79 | run: | 80 | mkdir -p priv/plts 81 | mix dialyzer --plt 82 | 83 | - name: Run dialyzer 84 | run: mix dialyzer --no-check --halt-exit-status 85 | 86 | - run: mix ecto.reset 87 | 88 | - name: Run tests 89 | run: mix test 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drill 2 | 3 | **Seed data handling for Elixir** 4 | 5 | Drill is an elixir seeder library inspired by [Seed Fu](https://github.com/mbleigh/seed-fu) and [Phinx](https://github.com/cakephp/phinx). 6 | 7 | ## Documentation 8 | 9 | [Official documentation on hexdocs](https://hexdocs.pm/drill/api-reference.html) 10 | 11 | ## Usage 12 | 13 | 1. Create your seeder modules. The directory where the seeder modules should be located 14 | is described on [mix drill documentation](https://hexdocs.pm/drill/Mix.Tasks.Drill.html). 15 | 16 | In `my_app/priv/repo/seeds/user.exs`: 17 | 18 | ```elixir 19 | defmodule MyApp.Seeds.User do 20 | use Drill, key: :users, source: MyApp.Accounts.User 21 | 22 | def factory do 23 | %{ 24 | first_name: Person.first_name(), 25 | last_name: Person.last_name() 26 | } 27 | end 28 | 29 | def run(_context) do 30 | [ 31 | seed(email: "email1@example.com"), 32 | seed(email: "email2@example.com"), 33 | seed(email: "email3@example.com") 34 | ] 35 | end 36 | end 37 | ``` 38 | 39 | In `my_app/priv/repo/seeds/post.exs`: 40 | 41 | ```elixir 42 | defmodule MyApp.Seeds.Post do 43 | use Drill, key: :posts, source: MyApp.Blogs.Post 44 | alias Faker.Lorem 45 | 46 | def deps do 47 | [MyApp.Seeds.User] 48 | end 49 | 50 | def factory do 51 | %{content: Lorem.paragraph()} 52 | end 53 | 54 | def run(%Drill.Context{seeds: %{users: [user1, user2, user3 | _]}}) do 55 | [ 56 | seed(user_id: user1.id), 57 | seed(user_id: user2.id), 58 | seed(user_id: user3.id) 59 | ] 60 | end 61 | end 62 | ``` 63 | 64 | 2. Run `mix drill -r MyApp.Repo` in the terminal with your project root as the current working directory 65 | 66 | ## Installation 67 | 68 | Add `drill` to your list of dependencies in `mix.exs`: 69 | 70 | ```elixir 71 | def deps do 72 | [ 73 | {:drill, "~> 1.2"} 74 | ] 75 | end 76 | ``` 77 | 78 | ## Configurations 79 | 80 | ### Timeout 81 | 82 | Default timeout is 600 seconds or 10 minutes. You may configure the task timeout in your config.exs file. For example: 83 | `config :drill, :timeout, 10_000` 84 | 85 | ## `use Drill` options 86 | 87 | - `source` (required) - source is the schema module 88 | - `key` (required) - once the seeder module runs, the inserted result will be saved to `%Drill.Context{}.seeds[key]`. 89 | Drill.Context struct is passed to one of Drill's callback which is `run/1` to be discussed in the `Callbacks` 90 | section below. 91 | - `returning` (optional) - selects which fields to return. Defaults to true. See [Ecto.Repo.insert_all/3](https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert_all/3) 92 | 93 | ## Callbacks 94 | 95 | - `constraints/0` (optional) - returns a list of column names to verify for conflicts. If a conflict occurs all fields will 96 | just be updated. This prevents insertion of new records based on the constraints when drill is run again. 97 | - `on_conflict/0` (optional) - returns the conflict strategy. The default is `:replace_all`. Only works when `constraints/0` 98 | returns a non-empty list. See [Ecto.Repo.insert_all/4](https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert_all/4) for more details. 99 | - `deps/0` (optional) - returns a list of seeder modules that should be run prior to the current seeder 100 | - `factory/0` (optional) - set default values for the fields. This is used when you call `seed/1` from the seeder module. 101 | - `run/1` (required) - returns a list of seeds (a call to `Drill.seed/1` function or anything you want to include in the context seed). 102 | Autogenerated fields such as `:inserted_at` or `:updated_at` may not be defined. The first argument is the `Drill.Context` struct, which 103 | you can use to get the inserted records from previously run seeder modules (see Usage section above). 104 | 105 | ## Command line options 106 | 107 | - `--repo` - specifies the repository to use 108 | - `--seeds-path` - overrides the default seeds path 109 | - Command line options for `mix app.start` documented [here](https://hexdocs.pm/mix/1.15.2/Mix.Tasks.App.Start.html#module-command-line-options) 110 | 111 | ## Caveat 112 | 113 | Can only be used on Postgres database for now 114 | 115 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 116 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 117 | be found at [https://hexdocs.pm/drill](https://hexdocs.pm/drill). 118 | -------------------------------------------------------------------------------- /lib/tasks/drill.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Drill do 2 | @moduledoc """ 3 | Runs all seeder modules under "priv/YOUR_REPO/seeds. Needs the Repo as an argument. 4 | $ mix drill -r MyApp.Repo 5 | 6 | Setting seeds directory is similar to [Ecto migration](https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Migrate.html), 7 | 1. "YOUR_REPO" is the last segment in your repository name. E.g. MyApp.MyRepo will use 8 | "priv/my_repo/seeds". 9 | 2. You can configure a repository to use another directory by specifying the :priv key under the repository configuration. 10 | The "seeds" part will be automatically appended to it. For instance, to use "priv/custom_repo/seeds": 11 | config :my_app, MyApp.Repo, priv: "priv/custom_repo" 12 | 3. You can also set the directory by adding the following to your config: 13 | config :drill, :directory, "seeds" 14 | 4. Lastly, you can override the path of the seeds directory by passing the --seeds-path option: 15 | $ mix drill -r MyApp.Repo --seeds-path priv/custom_repo/seeds 16 | """ 17 | 18 | @shortdoc "Seeding task" 19 | 20 | @switches [seeds_path: :string, prefix: :string] 21 | 22 | use Mix.Task 23 | import Mix.Ecto 24 | 25 | alias Drill.Seeder 26 | alias Drill.Utils 27 | 28 | @impl Mix.Task 29 | def run(args) do 30 | repo = parse_repo(args) |> hd() 31 | ensure_repo(repo, []) 32 | {opts, _} = OptionParser.parse!(args, switches: @switches) 33 | 34 | opts = 35 | opts 36 | |> Keyword.put(:task_timeout, Application.get_env(:drill, :timeout, 600_000)) 37 | |> Keyword.put_new( 38 | :seeds_path, 39 | Seeder.seeders_path(repo, Application.get_env(:drill, :directory, "seeds")) 40 | ) 41 | 42 | # Start the app 43 | Mix.Task.run("app.start", args) 44 | 45 | # Run the seed 46 | seed(repo, opts) 47 | end 48 | 49 | defp seed(repo, opts) do 50 | seeder_modules = Seeder.list_seeder_modules(opts[:seeds_path]) 51 | prefix = opts[:prefix] 52 | ensure_deps_exists!(seeder_modules) 53 | 54 | Mix.shell().info("Arranging modules by dependencies") 55 | 56 | seeder_modules = Utils.sort_seeders_by_deps(seeder_modules) 57 | 58 | Task.async(fn -> 59 | Enum.reduce(seeder_modules, %Drill.Context{repo: repo, prefix: prefix}, fn seeder, ctx -> 60 | Mix.shell().info("#{seeder} started") 61 | 62 | key = seeder.context_key() 63 | constraints = seeder.constraints() 64 | on_conflict = seeder.on_conflict() 65 | source = seeder.schema() 66 | returning = seeder.returning() 67 | 68 | autogenerated_fields = Seeder.autogenerate_fields(source) 69 | seeds = seeder.run(ctx) 70 | manual_seeds = Seeder.filter_manual_seeds(seeds) 71 | 72 | entries = 73 | seeds 74 | |> Seeder.build_entries_from_seeds() 75 | |> Utils.merge_autogenerated_fields_to_entries(autogenerated_fields) 76 | 77 | {_, result} = 78 | insert_all(repo, source, entries, 79 | conflict_target: constraints, 80 | on_conflict: on_conflict, 81 | returning: returning, 82 | prefix: prefix 83 | ) 84 | 85 | seeds = Map.put(ctx.seeds, key, manual_seeds ++ result) 86 | Mix.shell().info("#{seeder} finished") 87 | %{ctx | seeds: seeds} 88 | end) 89 | end) 90 | |> Task.await(opts[:task_timeout]) 91 | 92 | Mix.shell().info("Drill seeded successfully") 93 | end 94 | 95 | defp insert_all(repo, source, entries, opts) do 96 | case opts[:conflict_target] do 97 | [] -> 98 | opts = Keyword.drop(opts, [:conflict_target, :on_conflict]) 99 | repo.insert_all(source, entries, opts) 100 | 101 | _ -> 102 | repo.insert_all(source, entries, opts) 103 | end 104 | end 105 | 106 | defp ensure_deps_exists!(seeders) do 107 | seeder_nonexisting_deps_map = 108 | seeders 109 | |> Enum.map(fn seeder -> 110 | deps = seeder.deps() 111 | {seeder, Enum.filter(deps, &(&1 not in seeders))} 112 | end) 113 | |> Map.new() 114 | 115 | any_non_existing_deps? = Enum.any?(seeder_nonexisting_deps_map, &(elem(&1, 1) !== [])) 116 | 117 | if any_non_existing_deps? do 118 | seeder_nonexisting_deps_map 119 | |> Enum.filter(&(elem(&1, 1) !== [])) 120 | |> Enum.each(fn {seeder, non_existing_deps} -> 121 | Mix.shell().error("#{seeder} dependencies cannot be found: #{inspect(non_existing_deps)}") 122 | end) 123 | 124 | raise("Drill failed to run") 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/drill.ex: -------------------------------------------------------------------------------- 1 | defmodule Drill do 2 | @moduledoc """ 3 | Drill is an elixir seeder library inspired by [Seed Fu](https://github.com/mbleigh/seed-fu) and [Phinx](https://github.com/cakephp/phinx). 4 | 5 | ## Usage 6 | 1. Create your seeder modules. The directory where the seeder modules should be located 7 | is described on [mix drill documentation](https://hexdocs.pm/drill/Mix.Tasks.Drill.html). 8 | 9 | In `my_app/priv/repo/seeds/user.exs`: 10 | 11 | ``` 12 | defmodule MyApp.Seeds.User do 13 | use Drill, key: :users, source: MyApp.Accounts.User 14 | 15 | def factory do 16 | %{ 17 | first_name: Person.first_name(), 18 | last_name: Person.last_name() 19 | } 20 | end 21 | 22 | def run(_context) do 23 | [ 24 | seed(email: "email1@example.com"), 25 | seed(email: "email2@example.com"), 26 | seed(email: "email3@example.com") 27 | ] 28 | end 29 | end 30 | ``` 31 | 32 | In `my_app/priv/repo/seeds/post.exs`: 33 | 34 | ``` 35 | defmodule MyApp.Seeds.Post do 36 | use Drill, key: :posts, source: MyApp.Blogs.Post 37 | alias Faker.Lorem 38 | 39 | def deps do 40 | [MyApp.Seeds.User] 41 | end 42 | 43 | def factory do 44 | %{content: Lorem.paragraph()} 45 | end 46 | 47 | def run(%Drill.Context{seeds: %{users: [user1, user2, user3 | _]}}) do 48 | [ 49 | seed(user_id: user1.id), 50 | seed(user_id: user2.id), 51 | seed(user_id: user3.id) 52 | ] 53 | end 54 | end 55 | ``` 56 | 57 | 2. Run `mix drill -r MyApp.Repo` in the terminal with your project root as the current working directory 58 | 59 | ## Installation 60 | Add `drill` to your list of dependencies in `mix.exs`: 61 | ``` 62 | def deps do 63 | [ 64 | {:drill, "~> 1.2"} 65 | ] 66 | end 67 | ``` 68 | 69 | ## Configurations 70 | ### Timeout 71 | Default timeout is 600 seconds or 10 minutes. You may configure the task timeout in your config.exs file e.g.: 72 | config :drill, :timeout, 10_000 73 | 74 | ## `use Drill` options 75 | * `source` (required) - source is the schema module 76 | * `key` (required) - once the seeder module runs, the inserted result will be saved to `%Drill.Context{}.seeds[key]`. 77 | Drill.Context struct is passed to one of Drill's callback which is `run/1` to be discussed in the `Callbacks` 78 | section below. 79 | * `returning` (optional) - selects which fields to return. Defaults to true. See [Ecto.Repo.insert_all/3](https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert_all/3) 80 | 81 | ## Callbacks 82 | * `constraints/0` (optional) - returns a list of column names to verify for conflicts. If a conflict occurs all fields will 83 | just be updated. This prevents insertion of new records based on the constraints when drill is run again. 84 | * `on_conflict/0` (optional) - returns the conflict strategy. The default is `:replace_all`. Only works when `constraints/0` 85 | returns a non-empty list. See [Ecto.Repo.insert_all/4](https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert_all/4) for more details. 86 | * `deps/0` (optional) - returns a list of seeder modules that should be run prior to the current seeder 87 | * `factory/0` (optional) - set default values for the fields. This is used when you call `seed/1` from the seeder module. 88 | * `run/1` (required) - returns a list of seeds (a call to `Drill.seed/1` function or anything you want to include in the context seed). 89 | Autogenerated fields such as `:inserted_at` or `:updated_at` may not be defined. The first argument is the `Drill.Context` struct, which 90 | you can use to get the inserted records from previously run seeder modules (see Usage section above). 91 | 92 | ## Command line options 93 | * `--repo` - specifies the repository to use 94 | * `--seeds-path` - overrides the default seeds path 95 | * `--prefix` - specifies the prefix to use for the database tables 96 | * Command line options for `mix app.start` documented [here](https://hexdocs.pm/mix/1.15.2/Mix.Tasks.App.Start.html#module-command-line-options) 97 | """ 98 | alias Drill.Context 99 | alias Drill.Seed 100 | 101 | @callback deps() :: [atom()] 102 | @callback run(Context.t()) :: [any()] 103 | @callback factory() :: map() 104 | @callback constraints() :: [atom()] | {:unsafe_fragment, binary()} 105 | @callback on_conflict() :: 106 | :raise 107 | | :nothing 108 | | :replace_all 109 | | {:replace_all_except, [atom()]} 110 | | {:replace, [atom()]} 111 | 112 | defmacro __using__(opts \\ []) when is_list(opts) do 113 | source = Keyword.fetch!(opts, :source) 114 | key = Keyword.fetch!(opts, :key) 115 | returning = Keyword.get(opts, :returning, true) 116 | 117 | quote do 118 | @behaviour Drill 119 | 120 | def context_key, do: unquote(key) 121 | def schema, do: unquote(source) 122 | def returning, do: unquote(returning) 123 | 124 | @impl Drill 125 | def constraints, do: [] 126 | 127 | @impl Drill 128 | def on_conflict, do: :replace_all 129 | 130 | @impl Drill 131 | def factory, do: %{} 132 | 133 | @impl Drill 134 | def deps, do: [] 135 | 136 | def seed(attrs \\ []), do: Seed.new(__MODULE__, attrs) 137 | 138 | defoverridable deps: 0, constraints: 0, on_conflict: 0, factory: 0 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, 4 | "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, 5 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 6 | "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 8 | "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, 9 | "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, 10 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 11 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 12 | "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, 13 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 14 | "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, 15 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 17 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 18 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 19 | "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, 20 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 21 | } 22 | --------------------------------------------------------------------------------