diff --git a/.formatter.exs b/.formatter.exs index d2cda26..c3c4524 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,5 @@ # Used by "mix format" [ + subdirectories: ["priv/*/migrations"], inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ] diff --git a/config/config.exs b/config/config.exs index d2227a8..d4c9f00 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,7 +1,7 @@ -use Mix.Config +import Config config :commanded_audit_middleware, ecto_repos: [Commanded.Middleware.Auditing.Repo], serializer: Commanded.Serialization.JsonSerializer -import_config "#{Mix.env}.exs" +import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index 550b2b4..f56761a 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :commanded_audit_middleware, Commanded.Middleware.Auditing.Repo, adapter: Ecto.Adapters.Postgres, diff --git a/lib/commanded/middleware/auditing.ex b/lib/commanded/middleware/auditing.ex index 09258c6..66a482b 100644 --- a/lib/commanded/middleware/auditing.ex +++ b/lib/commanded/middleware/auditing.ex @@ -31,19 +31,18 @@ defmodule Commanded.Middleware.Auditing do metadata: metadata } = pipeline - audit = %CommandAudit{ - command_uuid: command_uuid, - causation_id: causation_id, - correlation_id: correlation_id, - occurred_at: occurred_at, - command_type: Atom.to_string(command.__struct__), - data: serialize(filter(command)), - metadata: serialize(metadata) - } - - Repo.insert!(audit) - - pipeline + audit = + Repo.insert!(%CommandAudit{ + command_uuid: command_uuid, + causation_id: causation_id, + correlation_id: correlation_id, + occurred_at: occurred_at, + command_type: Atom.to_string(command.__struct__), + data: serialize(filter(command)), + metadata: serialize(metadata) + }) + + assign(pipeline, :command_audit, audit) end defp success(%Pipeline{} = pipeline) do diff --git a/lib/commanded/middleware/auditing/command_audit.ex b/lib/commanded/middleware/auditing/command_audit.ex index 6af0558..e40ebac 100644 --- a/lib/commanded/middleware/auditing/command_audit.ex +++ b/lib/commanded/middleware/auditing/command_audit.ex @@ -1,17 +1,18 @@ defmodule Commanded.Middleware.Auditing.CommandAudit do use Ecto.Schema - @data_column_schema_type Application.get_env( + @data_column_schema_type Application.compile_env( :commanded_audit_middleware, :data_column_schema_type, :binary ) - @metadata_column_schema_type Application.get_env( + @metadata_column_schema_type Application.compile_env( :commanded_audit_middleware, :metadata_column_schema_type, :binary ) + @schema_prefix Application.compile_env(:commanded_audit_middleware, :prefix, "public") @primary_key {:command_uuid, :string, []} schema "command_audit" do diff --git a/lib/commanded/middleware/auditing/uuid.ex b/lib/commanded/middleware/auditing/uuid.ex new file mode 100644 index 0000000..62b994c --- /dev/null +++ b/lib/commanded/middleware/auditing/uuid.ex @@ -0,0 +1,104 @@ +# +# Implementation from https://github.com/elixir-ecto/ecto/blob/v3.8.4/lib/ecto/uuid.ex +# Apache License 2.0 https://github.com/elixir-ecto/ecto/blob/master/LICENSE.md +# +defmodule Commanded.Middleware.Auditing.UUID do + @moduledoc """ + Generate and format random, version 4 UUIDs. + """ + + @typedoc """ + A hex-encoded UUID string. + """ + @type hex :: <<_::288>> + + @typedoc """ + A raw binary representation of a UUID. + """ + @type raw :: <<_::128>> + + @doc """ + Generates a random, version 4 UUID. + """ + @spec uuid4() :: hex() + def uuid4, do: bingenerate() |> hex_encode() + + @doc """ + Converts a string representing a UUID into a raw binary. + """ + @spec string_to_binary!(hex()) :: raw() + def string_to_binary!( + <> + ) do + <> + end + + @spec binary_to_string!(raw()) :: hex() + def binary_to_string!(<<_::128>> = raw_uuid) do + hex_encode(raw_uuid) + end + + defp bingenerate do + <> = :crypto.strong_rand_bytes(16) + <> + end + + defp hex_encode( + <> + ) do + <> + end + + @compile {:inline, d: 1} + + defp d(?0), do: 0 + defp d(?1), do: 1 + defp d(?2), do: 2 + defp d(?3), do: 3 + defp d(?4), do: 4 + defp d(?5), do: 5 + defp d(?6), do: 6 + defp d(?7), do: 7 + defp d(?8), do: 8 + defp d(?9), do: 9 + defp d(?A), do: 10 + defp d(?B), do: 11 + defp d(?C), do: 12 + defp d(?D), do: 13 + defp d(?E), do: 14 + defp d(?F), do: 15 + defp d(?a), do: 10 + defp d(?b), do: 11 + defp d(?c), do: 12 + defp d(?d), do: 13 + defp d(?e), do: 14 + defp d(?f), do: 15 + defp d(_), do: throw(:error) + + @compile {:inline, e: 1} + + defp e(0), do: ?0 + defp e(1), do: ?1 + defp e(2), do: ?2 + defp e(3), do: ?3 + defp e(4), do: ?4 + defp e(5), do: ?5 + defp e(6), do: ?6 + defp e(7), do: ?7 + defp e(8), do: ?8 + defp e(9), do: ?9 + defp e(10), do: ?a + defp e(11), do: ?b + defp e(12), do: ?c + defp e(13), do: ?d + defp e(14), do: ?e + defp e(15), do: ?f +end diff --git a/mix.exs b/mix.exs index 9138180..e44dc36 100644 --- a/mix.exs +++ b/mix.exs @@ -48,7 +48,6 @@ defmodule Commanded.Middleware.Auditing.Mixfile do {:commanded, "~> 1.0", runtime: false}, {:ecto, "~> 3.3"}, {:ecto_sql, "~> 3.3"}, - {:elixir_uuid, "~> 1.2"}, {:ex_doc, ">= 0.0.0", only: :dev}, {:jason, "~> 1.1"}, {:postgrex, "~> 0.15"} diff --git a/mix.lock b/mix.lock index 2494f62..c3ee73a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,17 +1,19 @@ %{ - "commanded": {:hex, :commanded, "1.0.0", "5cd2c9b952ba95d09bb7310a0a0223fd24c5788ef63945a12d97909c42cb6d96", [:mix], [{:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}], "hexpm"}, - "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, - "db_connection": {:hex, :db_connection, "2.2.0", "e923e88887cd60f9891fd324ac5e0290954511d090553c415fbf54be4c57ee63", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, - "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "3.3.0", "9193e261d25c1814324d0b3304fccbadab840b286d270c3b75dfd28c30a3ae15", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, - "ecto_sql": {:hex, :ecto_sql, "3.3.2", "92804e0de69bb63e621273c3492252cb08a29475c05d40eeb6f41ad2d483cfd3", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, - "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"}, - "postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [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"}, - "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"}, + "backoff": {:hex, :backoff, "1.1.6", "83b72ed2108ba1ee8f7d1c22e0b4a00cfe3593a67dbc792799e8cce9f42f796b", [:rebar3], [], "hexpm", "cf0cfff8995fb20562f822e5cc47d8ccf664c5ecdc26a684cbe85c225f9d7c39"}, + "commanded": {:hex, :commanded, "1.4.1", "928b8357ebe1817f88b109693b4d717d20c11ef45cebe42a71dee0a56be36c2c", [:mix], [{:backoff, "~> 1.1", [hex: :backoff, repo: "hexpm", optional: false]}, {:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "6cd94b4b3369871c030a83b934548720cc834ec7b8549ba031510120aceb7ef9"}, + "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, + "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, + "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, + "ecto": {:hex, :ecto, "3.9.1", "67173b1687afeb68ce805ee7420b4261649d5e2deed8fe5550df23bab0bc4396", [:mix], [{:decimal, "~> 1.6 or ~> 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", "c80bb3d736648df790f7f92f81b36c922d9dd3203ca65be4ff01d067f54eb304"}, + "ecto_sql": {:hex, :ecto_sql, "3.9.0", "2bb21210a2a13317e098a420a8c1cc58b0c3421ab8e3acfa96417dab7817918c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 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", "a8f3f720073b8b1ac4c978be25fa7960ed7fd44997420c304a4a2e200b596453"}, + "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {: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", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "telemetry_registry": {:hex, :telemetry_registry, "0.3.0", "6768f151ea53fc0fbca70dbff5b20a8d663ee4e0c0b2ae589590e08658e76f1e", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "492e2adbc609f3e79ece7f29fec363a97a2c484ac78a83098535d6564781e917"}, } diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs new file mode 100644 index 0000000..49f9151 --- /dev/null +++ b/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/priv/repo/migrations/20161201130855_create_command_audit_table.exs b/priv/repo/migrations/20161201130855_create_command_audit_table.exs index ee75b40..04b876b 100644 --- a/priv/repo/migrations/20161201130855_create_command_audit_table.exs +++ b/priv/repo/migrations/20161201130855_create_command_audit_table.exs @@ -1,19 +1,22 @@ defmodule Commanded.Middleware.Auditing.Repo.Migrations.CreateCommandAuditTable do use Ecto.Migration - @data_column_db_type Application.get_env( + @prefix Application.compile_env(:commanded_audit_middleware, :prefix, "public") + + @data_column_db_type Application.compile_env( :commanded_audit_middleware, :data_column_db_type, :bytea ) - @metadata_column_db_type Application.get_env( + + @metadata_column_db_type Application.compile_env( :commanded_audit_middleware, :metadata_column_db_type, :bytea ) def change do - create table(:command_audit, primary_key: false) do + create table(:command_audit, primary_key: false, prefix: @prefix) do add(:command_uuid, :text, primary_key: true) add(:occurred_at, :naive_datetime) add(:command_type, :text) @@ -25,10 +28,10 @@ defmodule Commanded.Middleware.Auditing.Repo.Migrations.CreateCommandAuditTable add(:error_reason, :text) end - create(index(:command_audit, [:occurred_at])) + create index(:command_audit, [:occurred_at], prefix: @prefix) end def down do - drop(table(:command_audit)) + drop table(:command_audit, prefix: @prefix) end end diff --git a/priv/repo/migrations/20171103105605_add_causation_correlation_ids.exs b/priv/repo/migrations/20171103105605_add_causation_correlation_ids.exs index 99b2254..60e405b 100644 --- a/priv/repo/migrations/20171103105605_add_causation_correlation_ids.exs +++ b/priv/repo/migrations/20171103105605_add_causation_correlation_ids.exs @@ -1,8 +1,10 @@ defmodule Commanded.Middleware.Auditing.Repo.Migrations.AddCausationCorrelationIds do use Ecto.Migration + @prefix Application.compile_env(:commanded_audit_middleware, :prefix, "public") + def change do - alter table(:command_audit) do + alter table(:command_audit, prefix: @prefix) do add :causation_id, :uuid add :correlation_id, :uuid end diff --git a/priv/repo/migrations/20190729144959_create_gin_index_on_commandstore_command_audit_data.exs b/priv/repo/migrations/20190729144959_create_gin_index_on_commandstore_command_audit_data.exs index 54a45ee..225d51a 100644 --- a/priv/repo/migrations/20190729144959_create_gin_index_on_commandstore_command_audit_data.exs +++ b/priv/repo/migrations/20190729144959_create_gin_index_on_commandstore_command_audit_data.exs @@ -1,17 +1,19 @@ defmodule Commanded.Middleware.Auditing.Repo.Migrations.CreateGinIndexOnCommandstoreCommandAuditData do use Ecto.Migration + @prefix Application.compile_env(:commanded_audit_middleware, :prefix, "public") + def up do if Application.get_env(:commanded_audit_middleware, :data_column_db_type, :bytea) == :jsonb do execute( - "CREATE INDEX IF NOT EXISTS command_audit_data_gin ON command_audit USING GIN (data);" + "CREATE INDEX IF NOT EXISTS command_audit_data_gin ON #{@prefix}.command_audit USING GIN (data);" ) end if Application.get_env(:commanded_audit_middleware, :metadata_column_db_type, :bytea) == :jsonb do execute( - "CREATE INDEX IF NOT EXISTS command_audit_metadata_gin ON command_audit USING GIN (metadata);" + "CREATE INDEX IF NOT EXISTS command_audit_metadata_gin ON #{@prefix}.command_audit USING GIN (metadata);" ) end end diff --git a/test/auditing_test.exs b/test/auditing_test.exs index be6c185..c7ad7ff 100644 --- a/test/auditing_test.exs +++ b/test/auditing_test.exs @@ -2,7 +2,7 @@ defmodule Commanded.Middleware.AuditingTest do use ExUnit.Case alias Commanded.Middleware.Auditing - alias Commanded.Middleware.Auditing.{CommandAudit, Repo} + alias Commanded.Middleware.Auditing.{CommandAudit, Repo, UUID} alias Commanded.Middleware.Pipeline defmodule Command do