Packages
phoenix
1.7.21
1.8.9
1.8.8
1.8.7
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.8.0-rc.4
1.8.0-rc.3
1.8.0-rc.2
1.8.0-rc.1
1.8.0-rc.0
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.7.0-rc.3
1.7.0-rc.2
1.7.0-rc.1
1.7.0-rc.0
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.6.0-rc.1
1.6.0-rc.0
1.5.15
1.5.14
1.5.13
1.5.12
1.5.11
1.5.10
1.5.9
1.5.8
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.5.0-rc.0
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.4.0-rc.3
1.4.0-rc.2
1.4.0-rc.1
1.4.0-rc.0
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.3.0-rc.3
1.3.0-rc.2
1.3.0-rc.1
1.3.0-rc.0
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.1
1.2.0-rc.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.17.1
0.17.0
0.16.1
0.16.0
0.15.0
0.14.0
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.2
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Productive. Reliable. Fast. A productive web framework that does not compromise speed or maintainability.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/mix/phoenix/schema.ex
defmodule Mix.Phoenix.Schema do
@moduledoc false
alias Mix.Phoenix.Schema
defstruct module: nil,
repo: nil,
repo_alias: nil,
table: nil,
collection: nil,
embedded?: false,
generate?: true,
opts: [],
alias: nil,
file: nil,
attrs: [],
string_attr: nil,
plural: nil,
singular: nil,
uniques: [],
redacts: [],
assocs: [],
types: [],
indexes: [],
defaults: [],
human_singular: nil,
human_plural: nil,
binary_id: false,
migration_defaults: nil,
migration?: false,
params: %{},
optionals: [],
sample_id: nil,
web_path: nil,
web_namespace: nil,
context_app: nil,
route_helper: nil,
route_prefix: nil,
api_route_prefix: nil,
migration_module: nil,
fixture_unique_functions: [],
fixture_params: [],
prefix: nil,
timestamp_type: :naive_datetime
@valid_types [
:integer,
:float,
:decimal,
:boolean,
:map,
:string,
:array,
:references,
:text,
:date,
:time,
:time_usec,
:naive_datetime,
:naive_datetime_usec,
:utc_datetime,
:utc_datetime_usec,
:uuid,
:binary,
:enum
]
def valid_types, do: @valid_types
def valid?(schema) do
schema =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/
end
def new(schema_name, schema_plural, cli_attrs, opts) do
ctx_app = opts[:context_app] || Mix.Phoenix.context_app()
otp_app = Mix.Phoenix.otp_app()
opts = Keyword.merge(Application.get_env(otp_app, :generators, []), opts)
base = Mix.Phoenix.context_base(ctx_app)
basename = Phoenix.Naming.underscore(schema_name)
module = Module.concat([base, schema_name])
repo = opts[:repo] || Module.concat([base, "Repo"])
repo_alias = if String.ends_with?(Atom.to_string(repo), ".Repo"), do: "", else: ", as: Repo"
file = Mix.Phoenix.context_lib_path(ctx_app, basename <> ".ex")
table = opts[:table] || schema_plural
{cli_attrs, uniques, redacts} = extract_attr_flags(cli_attrs)
{assocs, attrs} = partition_attrs_and_assocs(module, attrs(cli_attrs))
types = types(attrs)
web_namespace = opts[:web] && Phoenix.Naming.camelize(opts[:web])
web_path = web_namespace && Phoenix.Naming.underscore(web_namespace)
api_prefix = Application.get_env(otp_app, :generators)[:api_prefix] || "/api"
embedded? = Keyword.get(opts, :embedded, false)
generate? = Keyword.get(opts, :schema, true)
singular =
module
|> Module.split()
|> List.last()
|> Phoenix.Naming.underscore()
collection = if schema_plural == singular, do: singular <> "_collection", else: schema_plural
string_attr = string_attr(types)
create_params = params(attrs, :create)
optionals = for {key, :map} <- types, do: key, into: []
default_params_key =
case Enum.at(create_params, 0) do
{key, _} -> key
nil -> :some_field
end
fixture_unique_functions = fixture_unique_functions(singular, uniques, attrs)
%Schema{
opts: opts,
migration?: Keyword.get(opts, :migration, true),
module: module,
repo: repo,
repo_alias: repo_alias,
table: table,
embedded?: embedded?,
alias: module |> Module.split() |> List.last() |> Module.concat(nil),
file: file,
attrs: attrs,
plural: schema_plural,
singular: singular,
collection: collection,
optionals: optionals,
assocs: assocs,
types: types,
defaults: schema_defaults(attrs),
uniques: uniques,
redacts: redacts,
indexes: indexes(table, assocs, uniques),
human_singular: Phoenix.Naming.humanize(singular),
human_plural: Phoenix.Naming.humanize(schema_plural),
binary_id: opts[:binary_id],
timestamp_type: opts[:timestamp_type] || :naive_datetime,
migration_defaults: migration_defaults(attrs),
string_attr: string_attr,
params: %{
create: create_params,
update: params(attrs, :update),
default_key: string_attr || default_params_key
},
web_namespace: web_namespace,
web_path: web_path,
route_helper: route_helper(web_path, singular),
route_prefix: route_prefix(web_path, schema_plural),
api_route_prefix: api_route_prefix(web_path, schema_plural, api_prefix),
sample_id: sample_id(opts),
context_app: ctx_app,
generate?: generate?,
migration_module: migration_module(),
fixture_unique_functions: Enum.sort(fixture_unique_functions),
fixture_params: fixture_params(attrs, fixture_unique_functions),
prefix: opts[:prefix]
}
end
@doc """
Returns the string value of the default schema param.
"""
def default_param(%Schema{} = schema, action) do
schema.params
|> Map.fetch!(action)
|> Map.fetch!(schema.params.default_key)
|> to_string()
end
def extract_attr_flags(cli_attrs) do
{attrs, uniques, redacts} =
Enum.reduce(cli_attrs, {[], [], []}, fn attr, {attrs, uniques, redacts} ->
[attr_name | rest] = String.split(attr, ":")
attr_name = String.to_atom(attr_name)
split_flags(Enum.reverse(rest), attr_name, attrs, uniques, redacts)
end)
{Enum.reverse(attrs), uniques, redacts}
end
defp split_flags(["unique" | rest], name, attrs, uniques, redacts),
do: split_flags(rest, name, attrs, [name | uniques], redacts)
defp split_flags(["redact" | rest], name, attrs, uniques, redacts),
do: split_flags(rest, name, attrs, uniques, [name | redacts])
defp split_flags(rest, name, attrs, uniques, redacts),
do: {[Enum.join([name | Enum.reverse(rest)], ":") | attrs], uniques, redacts}
@doc """
Parses the attrs as received by generators.
"""
def attrs(attrs) do
Enum.map(attrs, fn attr ->
attr
|> String.split(":", parts: 3)
|> list_to_attr()
|> validate_attr!()
end)
end
@doc """
Generates some sample params based on the parsed attributes.
"""
def params(attrs, action \\ :create) when action in [:create, :update] do
Map.new(attrs, fn {k, t} -> {k, type_to_default(k, t, action)} end)
end
@doc """
Converts the given value to map format when it is a date, time, datetime or naive_datetime.
Since `form_component.html.heex` generated by the live generator uses selects for dates and/or
times, fixtures must use map format for those fields in order to submit the live form.
"""
def live_form_value(%Date{} = date), do: Calendar.strftime(date, "%Y-%m-%d")
def live_form_value(%Time{} = time), do: Calendar.strftime(time, "%H:%M")
def live_form_value(%NaiveDateTime{} = naive) do
NaiveDateTime.to_iso8601(naive)
end
def live_form_value(%DateTime{} = naive) do
DateTime.to_iso8601(naive)
end
def live_form_value(value), do: value
@doc """
Build an invalid value for `@invalid_attrs` which is nil by default.
* In case the value is a list, this will return an empty array.
* In case the value is date, datetime, naive_datetime or time, this will return an invalid date.
* In case it is a boolean, we keep it as false
"""
def invalid_form_value(value) when is_list(value), do: []
def invalid_form_value(%{day: _day, month: _month, year: _year} = _date),
do: "2022-00"
def invalid_form_value(%{hour: _hour, minute: _minute}), do: %{hour: 14, minute: 00}
def invalid_form_value(true), do: false
def invalid_form_value(_value), do: nil
@doc """
Generates an invalid error message according to the params present in the schema.
"""
def failed_render_change_message(_schema) do
"can't be blank"
end
def type_for_migration({:enum, _}), do: :string
def type_for_migration(other), do: other
def format_fields_for_schema(schema) do
Enum.map_join(schema.types, "\n", fn {k, v} ->
" field #{inspect(k)}, #{type_and_opts_for_schema(v)}#{schema.defaults[k]}#{maybe_redact_field(k in schema.redacts)}"
end)
end
@doc """
Return the required fields in the schema. Anything not in the `optionals` list
is considered required.
"""
def required_fields(schema) do
Enum.reject(schema.attrs, fn {key, _} -> key in schema.optionals end)
end
def type_and_opts_for_schema({:enum, opts}),
do: ~s|Ecto.Enum, values: #{inspect(Keyword.get(opts, :values))}|
def type_and_opts_for_schema(other), do: inspect(other)
def maybe_redact_field(true), do: ", redact: true"
def maybe_redact_field(false), do: ""
@doc """
Returns the string value for use in EEx templates.
"""
def value(schema, field, value) do
schema.types
|> Map.fetch!(field)
|> inspect_value(value)
end
defp inspect_value(:decimal, value), do: "Decimal.new(\"#{value}\")"
defp inspect_value(_type, value), do: inspect(value)
defp list_to_attr([key]), do: {String.to_atom(key), :string}
defp list_to_attr([key, value]), do: {String.to_atom(key), String.to_atom(value)}
defp list_to_attr([key, comp, value]) do
{String.to_atom(key), {String.to_atom(comp), String.to_atom(value)}}
end
@one_day_in_seconds 24 * 3600
defp type_to_default(key, t, :create) do
case t do
{:array, type} ->
build_array_values(type, :create)
{:enum, values} ->
build_enum_values(values, :create)
:integer ->
42
:float ->
120.5
:decimal ->
"120.5"
:boolean ->
true
:map ->
%{}
:text ->
"some #{key}"
:date ->
Date.add(Date.utc_today(), -1)
:time ->
~T[14:00:00]
:time_usec ->
~T[14:00:00.000000]
:uuid ->
"7488a646-e31f-11e4-aace-600308960662"
:utc_datetime ->
DateTime.add(
build_utc_datetime(),
-@one_day_in_seconds,
:second,
Calendar.UTCOnlyTimeZoneDatabase
)
:utc_datetime_usec ->
DateTime.add(
build_utc_datetime_usec(),
-@one_day_in_seconds,
:second,
Calendar.UTCOnlyTimeZoneDatabase
)
:naive_datetime ->
NaiveDateTime.add(build_utc_naive_datetime(), -@one_day_in_seconds)
:naive_datetime_usec ->
NaiveDateTime.add(build_utc_naive_datetime_usec(), -@one_day_in_seconds)
_ ->
"some #{key}"
end
end
defp type_to_default(key, t, :update) do
case t do
{:array, type} -> build_array_values(type, :update)
{:enum, values} -> build_enum_values(values, :update)
:integer -> 43
:float -> 456.7
:decimal -> "456.7"
:boolean -> false
:map -> %{}
:text -> "some updated #{key}"
:date -> Date.utc_today()
:time -> ~T[15:01:01]
:time_usec -> ~T[15:01:01.000000]
:uuid -> "7488a646-e31f-11e4-aace-600308960668"
:utc_datetime -> build_utc_datetime()
:utc_datetime_usec -> build_utc_datetime_usec()
:naive_datetime -> build_utc_naive_datetime()
:naive_datetime_usec -> build_utc_naive_datetime_usec()
_ -> "some updated #{key}"
end
end
defp build_array_values(:string, :create),
do: Enum.map([1, 2], &"option#{&1}")
defp build_array_values(:integer, :create),
do: [1, 2]
defp build_array_values(:string, :update),
do: ["option1"]
defp build_array_values(:integer, :update),
do: [1]
defp build_array_values(_, _),
do: []
defp build_enum_values(values, action) do
case {action, translate_enum_vals(values)} do
{:create, vals} -> hd(vals)
{:update, [val | []]} -> val
{:update, vals} -> vals |> tl() |> hd()
end
end
defp build_utc_datetime_usec,
do: %{DateTime.utc_now() | second: 0, microsecond: {0, 6}}
defp build_utc_datetime,
do: DateTime.truncate(build_utc_datetime_usec(), :second)
defp build_utc_naive_datetime_usec,
do: %{NaiveDateTime.utc_now() | second: 0, microsecond: {0, 6}}
defp build_utc_naive_datetime,
do: NaiveDateTime.truncate(build_utc_naive_datetime_usec(), :second)
@enum_missing_value_error """
Enum type requires at least one value
For example:
mix phx.gen.schema Comment comments body:text status:enum:published:unpublished
"""
defp validate_attr!({name, :datetime}), do: validate_attr!({name, :naive_datetime})
defp validate_attr!({name, :array}) do
Mix.raise("""
Phoenix generators expect the type of the array to be given to #{name}:array.
For example:
mix phx.gen.schema Post posts settings:array:string
""")
end
defp validate_attr!({_name, :enum}), do: Mix.raise(@enum_missing_value_error)
defp validate_attr!({_name, type} = attr) when type in @valid_types, do: attr
defp validate_attr!({_name, {:enum, _vals}} = attr), do: attr
defp validate_attr!({_name, {type, _}} = attr) when type in @valid_types, do: attr
defp validate_attr!({_, type}) do
Mix.raise(
"Unknown type `#{inspect(type)}` given to generator. " <>
"The supported types are: #{@valid_types |> Enum.sort() |> Enum.join(", ")}"
)
end
defp partition_attrs_and_assocs(schema_module, attrs) do
{assocs, attrs} =
Enum.split_with(attrs, fn
{_, {:references, _}} ->
true
{key, :references} ->
Mix.raise("""
Phoenix generators expect the table to be given to #{key}:references.
For example:
mix phx.gen.schema Comment comments body:text post_id:references:posts
""")
_ ->
false
end)
assocs =
Enum.map(assocs, fn {key_id, {:references, source}} ->
key = String.replace(Atom.to_string(key_id), "_id", "")
base = schema_module |> Module.split() |> Enum.drop(-1)
module = Module.concat(base ++ [Phoenix.Naming.camelize(key)])
{String.to_atom(key), key_id, inspect(module), source}
end)
{assocs, attrs}
end
defp schema_defaults(attrs) do
Enum.into(attrs, %{}, fn
{key, :boolean} -> {key, ", default: false"}
{key, _} -> {key, ""}
end)
end
defp string_attr(types) do
Enum.find_value(types, fn
{key, :string} -> key
_ -> false
end)
end
defp types(attrs) do
Enum.into(attrs, %{}, fn
{key, {:enum, vals}} -> {key, {:enum, values: translate_enum_vals(vals)}}
{key, {root, val}} -> {key, {root, schema_type(val)}}
{key, val} -> {key, schema_type(val)}
end)
end
def translate_enum_vals(vals) do
vals
|> Atom.to_string()
|> String.split(":")
|> Enum.map(&String.to_atom/1)
end
defp schema_type(:text), do: :string
defp schema_type(:uuid), do: Ecto.UUID
defp schema_type(val) do
if Code.ensure_loaded?(Ecto.Type) and not Ecto.Type.primitive?(val) do
Mix.raise("Unknown type `#{val}` given to generator")
else
val
end
end
defp indexes(table, assocs, uniques) do
uniques = Enum.map(uniques, fn key -> {key, true} end)
assocs = Enum.map(assocs, fn {_, key, _, _} -> {key, false} end)
(uniques ++ assocs)
|> Enum.uniq_by(fn {key, _} -> key end)
|> Enum.map(fn
{key, false} -> "create index(:#{table}, [:#{key}])"
{key, true} -> "create unique_index(:#{table}, [:#{key}])"
end)
end
defp migration_defaults(attrs) do
Enum.into(attrs, %{}, fn
{key, :boolean} -> {key, ", default: false, null: false"}
{key, _} -> {key, ""}
end)
end
defp sample_id(opts) do
if Keyword.get(opts, :binary_id, false) do
Keyword.get(opts, :sample_binary_id, "11111111-1111-1111-1111-111111111111")
else
-1
end
end
defp route_helper(web_path, singular) do
"#{web_path}_#{singular}"
|> String.trim_leading("_")
|> String.replace("/", "_")
end
defp route_prefix(web_path, plural) do
path = Path.join(for str <- [web_path, plural], do: to_string(str))
"/" <> String.trim_leading(path, "/")
end
defp api_route_prefix(web_path, plural, api_prefix) do
path = Path.join(for str <- [api_prefix, web_path, plural], do: to_string(str))
"/" <> String.trim_leading(path, "/")
end
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
defp fixture_unique_functions(singular, uniques, attrs) do
uniques
|> Enum.filter(&Keyword.has_key?(attrs, &1))
|> Enum.into(%{}, fn attr ->
function_name = "unique_#{singular}_#{attr}"
{function_def, needs_impl?} =
case Keyword.fetch!(attrs, attr) do
:integer ->
function_def =
"""
def #{function_name}, do: System.unique_integer([:positive])
"""
{function_def, false}
type when type in [:string, :text] ->
function_def =
"""
def #{function_name}, do: "some #{attr}\#{System.unique_integer([:positive])}"
"""
{function_def, false}
_ ->
function_def =
"""
def #{function_name} do
raise "implement the logic to generate a unique #{singular} #{attr}"
end
"""
{function_def, true}
end
{attr, {function_name, function_def, needs_impl?}}
end)
end
defp fixture_params(attrs, fixture_unique_functions) do
attrs
|> Enum.sort()
|> Enum.map(fn {attr, type} ->
case fixture_unique_functions do
%{^attr => {function_name, _function_def, _needs_impl?}} ->
{attr, "#{function_name}()"}
%{} ->
{attr, inspect(type_to_default(attr, type, :create))}
end
end)
end
end