Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ecto migration generator for new flags #182

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# FunWithFlags

[![Mix Tests](https://github.com/tompave/fun_with_flags/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/tompave/fun_with_flags/actions/workflows/test.yml?query=branch%3Amaster)
[![Code Quality](https://github.com/tompave/fun_with_flags/actions/workflows/quality.yml/badge.svg?branch=master)](https://github.com/tompave/fun_with_flags/actions/workflows/quality.yml?query=branch%3Amaster)
[![Code Quality](https://github.com/tompave/fun_with_flags/actions/workflows/quality.yml/badge.svg?branch=master)](https://github.com/tompave/fun_with_flags/actions/workflows/quality.yml?query=branch%3Amaster)
[![Hex.pm](https://img.shields.io/hexpm/v/fun_with_flags.svg)](https://hex.pm/packages/fun_with_flags)
[![hexdocs.pm](https://img.shields.io/badge/docs-1.11.0-brightgreen.svg)](https://hexdocs.pm/fun_with_flags/1.11.0/FunWithFlags.html)
[![Hex.pm Downloads](https://img.shields.io/hexpm/dt/fun_with_flags)](https://hex.pm/packages/fun_with_flags)
Expand Down Expand Up @@ -646,7 +646,7 @@ The library comes with two PubSub adapters for the [`Redix`](https://hex.pm/pack

The Redis PubSub adapter is the default and doesn't need to be explicitly configured. It can only be used in conjunction with the Redis persistence adapter however, and is not available when using Ecto for persistence. When used, it connects directly to the Redis instance used for persisting the flag data.

The Phoenix PubSub adapter uses the high level API of `Phoenix.PubSub`, which means that under the hood it could use either its PG2 or Redis adapters, and this library doesn't need to know. It's provided as a convenient way to leverage distributed Erlang when using FunWithFlags in a Phoenix application, although it can be used independently (without the rest of the Phoenix framework) to add PubSub to Elixir apps running on Erlang clusters.
The Phoenix PubSub adapter uses the high level API of `Phoenix.PubSub`, which means that under the hood it could use either its PG2 or Redis adapters, and this library doesn't need to know. It's provided as a convenient way to leverage distributed Erlang when using FunWithFlags in a Phoenix application, although it can be used independently (without the rest of the Phoenix framework) to add PubSub to Elixir apps running on Erlang clusters.
FunWithFlags expects the `Phoenix.PubSub` process to be started by the host application, and in order to use this adapter the client (name or PID) must be provided in the configuration.

For example, in Phoenix (>= 1.5.0) it would be:
Expand Down Expand Up @@ -839,3 +839,9 @@ Steps:
5. In either terminal, run `Node.list()` to check that there is a connection.

Done that, modifying any flag data in either terminal will notify the other one via PubSub.

### Creating flags with migrations

If you store flags in database and you want to define flags using migrations instead of creating them manually, you can use `mix fwf.gen.flag FlagName` to create a migration which will populate flag with this name into the database.

After this if you use [web dashboard](#web-dashboard), you can manually change values for the flag without the need to create it.
134 changes: 134 additions & 0 deletions lib/mix/tasks/fwf.gen.flag.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
defmodule Mix.Tasks.Fwf.Gen.Flag do
@shortdoc "Generates a new migration for creating a feature flag"

@moduledoc """
Generates a flag migration.

## Examples

$ mix fwf.gen.flag AwesomeFeature

The generated migration filename will be prefixed with the current
timestamp in UTC which is used for versioning and ordering.

By default, the migration will be generated to the
"priv/YOUR_REPO/migrations" directory of the current application
but it can be configured to be any subdirectory of `priv` by
specifying the `:priv` key under the repository configuration.

## Command line options

* `--migrations-path` - the path to run the migrations from, defaults to `priv/repo/migrations`

"""

use Mix.Task

import Macro, only: [camelize: 1, underscore: 1]
import Mix.EctoSQL
import Mix.Generator

@switches [
migrations_path: :string
]

@impl true
def run(args) do
repo = Application.fetch_env!(:fun_with_flags, :persistence)[:repo]

unless FunWithFlags.Config.persist_in_ecto?() do
Mix.raise("You need to configure FunWithFlags to persist in Ecto to use this task.")
end

case OptionParser.parse!(args, strict: @switches) do
{opts, [name]} ->
table_name = Application.fetch_env!(:fun_with_flags, :persistence)[:ecto_table_name]
primary_key_type = Application.fetch_env!(:fun_with_flags, :persistence)[:ecto_primary_key_type]

path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations")
flag_name = underscore(name)
base_name = "add_feature_flag_#{flag_name}.exs"
file = Path.join(path, "#{timestamp()}_#{base_name}")
unless File.dir?(path), do: create_directory(path)

fuzzy_path = Path.join(path, "*_#{base_name}")

if Path.wildcard(fuzzy_path) != [] do
Mix.raise("migration can't be created, there is already a migration file with name #{name}.")
end

assigns = [
mod: Module.concat([repo, Migrations, camelize(name)]),
table_name: table_name,
primary_key_type: primary_key_type,
flag_name: flag_name
]

create_file(file, migration_template(assigns))

file

{_, _} ->
Mix.raise(
"expected fwf.gen.flag to receive the migration file name, " <>
"got: #{inspect(Enum.join(args, " "))}"
)
end
end

defp timestamp do
{{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
end

defp pad(i) when i < 10, do: <<?0, ?0 + i>>
defp pad(i), do: to_string(i)

defp migration_module do
case Application.get_env(:ecto_sql, :migration_module, Ecto.Migration) do
migration_module when is_atom(migration_module) -> migration_module
other -> Mix.raise("Expected :migration_module to be a module, got: #{inspect(other)}")
end
end

embed_template(:migration, """
defmodule <%= inspect @mod %> do
use <%= inspect migration_module() %>

require Ecto.Query

defmodule FeatureFlagSchema do
@moduledoc false
use Ecto.Schema

@primary_key {:id, <%= inspect @primary_key_type %>, autogenerate: true}

schema <%= inspect @table_name %> do
field(:flag_name, :string)
field(:gate_type, :string)
field(:target, :string)
field(:enabled, :boolean)
end
end

def up do
repo().insert_all(FeatureFlagSchema, [
%{
flag_name: <%= inspect @flag_name %>,
gate_type: "boolean",
target: "_fwf_none",
enabled: false
}
])
end

def down do
Ecto.Query.from(
FeatureFlagSchema,
where: [flag_name: <%= inspect @flag_name %>]
)
|> repo().delete_all()
end
end
""")
end