mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-19 13:03:14 +12:00
feat: mix ash_postgres.gen.resources
This commit is contained in:
parent
0ccb35a713
commit
133be5094b
12 changed files with 2002 additions and 8 deletions
33
documentation/tutorials/set-up-with-existing-database.md
Normal file
33
documentation/tutorials/set-up-with-existing-database.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Setting AshPostgres up with an existing database
|
||||
|
||||
If you already have a postgres database and you'd like to get
|
||||
started quickly, you can scaffold resources directly from your
|
||||
database.
|
||||
|
||||
First, create an application with AshPostgres if you haven't already:
|
||||
|
||||
```bash
|
||||
mix igniter.new my_app
|
||||
--install ash,ash_postgres
|
||||
--with phx.new # add this if you will be using phoenix too
|
||||
```
|
||||
|
||||
Then, go into your `config/dev.exs` and configure your repo to use
|
||||
your existing database.
|
||||
|
||||
Finally, run:
|
||||
|
||||
```bash
|
||||
mix ash_postgres.gen.resources MyApp.MyDomain --tables table1,table2,table3
|
||||
```
|
||||
|
||||
## More fine grained control
|
||||
|
||||
You may want to do multiple passes to separate your application into multiple domains. For example:
|
||||
|
||||
```bash
|
||||
mix ash_postgres.gen.resources MyApp.Accounts --tables users,roles,tokens
|
||||
mix ash_postgres.gen.resources MyApp.Blog --tables posts,comments
|
||||
```
|
||||
|
||||
See the docs for `mix ash_postgres.gen.resources` for more information.
|
|
@ -18,6 +18,30 @@ defmodule AshPostgres.Igniter do
|
|||
"""
|
||||
end
|
||||
|
||||
def table(igniter, resource) do
|
||||
igniter
|
||||
|> Spark.Igniter.get_option(resource, [:postgres, :table])
|
||||
|> case do
|
||||
{igniter, {:ok, value}} when is_binary(value) or is_nil(value) ->
|
||||
{:ok, igniter, value}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def repo(igniter, resource) do
|
||||
igniter
|
||||
|> Spark.Igniter.get_option(resource, [:postgres, :repo])
|
||||
|> case do
|
||||
{igniter, {:ok, value}} when is_atom(value) ->
|
||||
{:ok, igniter, value}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def add_postgres_extension(igniter, repo_name, extension) do
|
||||
Igniter.Code.Module.find_and_update_module!(igniter, repo_name, fn zipper ->
|
||||
case Igniter.Code.Function.move_to_def(zipper, :installed_extensions, 0) do
|
||||
|
|
|
@ -18,6 +18,7 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
format: true,
|
||||
dry_run: false,
|
||||
check: false,
|
||||
snapshots_only: false,
|
||||
dont_drop_columns: false
|
||||
|
||||
def generate(domains, opts \\ []) do
|
||||
|
|
137
lib/mix/tasks/ash_postgres.gen.resources.ex
Normal file
137
lib/mix/tasks/ash_postgres.gen.resources.ex
Normal file
|
@ -0,0 +1,137 @@
|
|||
defmodule Mix.Tasks.AshPostgres.Gen.Resources do
|
||||
use Igniter.Mix.Task
|
||||
|
||||
@example "mix ash_postgres.gen.resource MyApp.MyDomain"
|
||||
|
||||
@shortdoc "Generates or updates resources based on a database schema"
|
||||
|
||||
@doc """
|
||||
#{@shortdoc}
|
||||
|
||||
## Example
|
||||
|
||||
`#{@example}`
|
||||
|
||||
## Domain
|
||||
|
||||
The domain will be generated if it does not exist. If you aren't sure,
|
||||
we suggest using something like `MyApp.App`.
|
||||
|
||||
## Options
|
||||
|
||||
- `repo`, `r` - The repo or repos to generate resources for, comma separated. Can be specified multiple times. Defaults to all repos.
|
||||
- `tables`, `t` - Defaults to `public.*`. The tables to generate resources for, comma separated. Can be specified multiple times. See the section on tables for more.
|
||||
- `skip-tables`, `s` - The tables to skip generating resources for, comma separated. Can be specified multiple times. See the section on tables for more.
|
||||
- `snapshots-only` - Only generate snapshots for the generated resources, and not migraitons.
|
||||
|
||||
## Tables
|
||||
|
||||
When specifying tables to include with `--tables`, you can specify the table name, or the schema and table name separated by a period.
|
||||
For example, `users` will generate resources for the `users` table in the `public` schema, but `accounts.users` will generate resources for the `users` table in the `accounts` schema.
|
||||
|
||||
To include all tables in a given schema, add a period only with no table name, i.e `schema.`, i.e `accounts.`.
|
||||
|
||||
When skipping tables with `--skip-tables`, the same rules apply, except that the `schema.` format is not supported.
|
||||
"""
|
||||
|
||||
@impl Igniter.Mix.Task
|
||||
def info(_argv, _parent) do
|
||||
%Igniter.Mix.Task.Info{
|
||||
positional: [:domain],
|
||||
example: @example,
|
||||
schema: [
|
||||
repo: :keep,
|
||||
tables: :keep,
|
||||
skip_tables: :keep,
|
||||
snapshots_only: :boolean,
|
||||
domain: :keep
|
||||
],
|
||||
aliases: [
|
||||
t: :tables,
|
||||
r: :repo,
|
||||
d: :domain,
|
||||
s: :skip_tables
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
@impl Igniter.Mix.Task
|
||||
def igniter(igniter, argv) do
|
||||
Mix.Task.run("compile")
|
||||
|
||||
{%{domain: domain}, argv} = positional_args!(argv)
|
||||
|
||||
domain = Igniter.Code.Module.parse(domain)
|
||||
|
||||
options = options!(argv)
|
||||
|
||||
repos =
|
||||
options[:repo] ||
|
||||
Mix.Project.config()[:app]
|
||||
|> Application.get_env(:ecto_repos, [])
|
||||
|
||||
case repos do
|
||||
[] ->
|
||||
igniter
|
||||
|> Igniter.add_warning("No ecto repos configured.")
|
||||
|
||||
repos ->
|
||||
Mix.shell().info("Generating resources from #{inspect(repos)}")
|
||||
|
||||
prompt =
|
||||
"""
|
||||
|
||||
Would you like to generate migrations for the current structure? (recommended)
|
||||
|
||||
If #{IO.ANSI.green()}yes#{IO.ANSI.reset()}:
|
||||
We will generate migrations based on the generated resources.
|
||||
You should then change your database name in your config, and
|
||||
run `mix ash.setup`.
|
||||
|
||||
If you already have ecto migrations you'd like to use, run
|
||||
this command with `--snapshots-only`, in which case only resource
|
||||
snapshots will be generated.
|
||||
#{IO.ANSI.green()}
|
||||
Going forward, your resources will be the source of truth.#{IO.ANSI.reset()}
|
||||
#{IO.ANSI.red()}
|
||||
*WARNING*
|
||||
|
||||
If you run `mix ash.reset` after this command without updating
|
||||
your config, you will be *deleting the database you just used to
|
||||
generate these resources*!#{IO.ANSI.reset()}
|
||||
|
||||
If #{IO.ANSI.red()}no#{IO.ANSI.reset()}:
|
||||
|
||||
We will not generate any migrations. This means you have migrations already that
|
||||
can get you from zero to the current starting point.
|
||||
#{IO.ANSI.yellow()}
|
||||
You will have to hand-write migrations from this point on.#{IO.ANSI.reset()}
|
||||
"""
|
||||
|
||||
options =
|
||||
if Mix.shell().yes?(prompt) do
|
||||
Keyword.put(options, :no_migrations, false)
|
||||
else
|
||||
Keyword.put(options, :no_migrations, true)
|
||||
end
|
||||
|
||||
migration_opts =
|
||||
if options[:snapshots_only] do
|
||||
["--snapshots-only"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
igniter
|
||||
|> Igniter.compose_task("ash.gen.domain", [inspect(domain), "--ignore-if-exists"])
|
||||
|> AshPostgres.ResourceGenerator.generate(repos, domain, options)
|
||||
|> then(fn igniter ->
|
||||
if options[:no_migrations] do
|
||||
igniter
|
||||
else
|
||||
Igniter.add_task(igniter, "ash_postgres.generate_migrations", migration_opts)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -21,6 +21,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do
|
|||
* `no-format` - files that are created will not be formatted with the code formatter
|
||||
* `dry-run` - no files are created, instead the new migration is printed
|
||||
* `check` - no files are created, returns an exit(1) code if the current snapshots and resources don't fit
|
||||
* `snapshots-only` - no migrations are generated, only snapshots are stored
|
||||
|
||||
#### Snapshots
|
||||
|
||||
|
@ -90,6 +91,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do
|
|||
migration_path: :string,
|
||||
tenant_migration_path: :string,
|
||||
quiet: :boolean,
|
||||
snapshots_only: :boolean,
|
||||
name: :string,
|
||||
no_format: :boolean,
|
||||
dry_run: :boolean,
|
||||
|
@ -100,7 +102,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do
|
|||
|
||||
domains = AshPostgres.Mix.Helpers.domains!(opts, args, false)
|
||||
|
||||
if Enum.empty?(domains) do
|
||||
if Enum.empty?(domains) && !opts[:snapshots_only] do
|
||||
IO.warn("""
|
||||
No domains found, so no resource-related migrations will be generated.
|
||||
Pass the `--domains` option or configure `config :your_app, ash_domains: [...]`
|
||||
|
@ -113,7 +115,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do
|
|||
|> Keyword.delete(:no_format)
|
||||
|> Keyword.put_new(:name, name)
|
||||
|
||||
if !opts[:name] && !opts[:dry_run] && !opts[:check] do
|
||||
if !opts[:name] && !opts[:dry_run] && !opts[:check] && !opts[:snapshots_only] do
|
||||
IO.warn("""
|
||||
Name must be provided when generating migrations, unless `--dry-run` or `--check` is also provided.
|
||||
Using an autogenerated name will be deprecated in a future release.
|
||||
|
|
658
lib/resource_generator/resource_generator.ex
Normal file
658
lib/resource_generator/resource_generator.ex
Normal file
|
@ -0,0 +1,658 @@
|
|||
defmodule AshPostgres.ResourceGenerator do
|
||||
alias AshPostgres.ResourceGenerator.Spec
|
||||
|
||||
require Logger
|
||||
|
||||
def generate(igniter, repos, domain, opts \\ []) do
|
||||
{igniter, resources} = Ash.Resource.Igniter.list_resources(igniter)
|
||||
|
||||
resources =
|
||||
Task.async_stream(resources, fn resource ->
|
||||
{resource, AshPostgres.Igniter.repo(igniter, resource),
|
||||
AshPostgres.Igniter.table(igniter, resource)}
|
||||
end)
|
||||
|> Enum.map(fn {:ok, {resource, repo, table}} ->
|
||||
repo =
|
||||
case repo do
|
||||
{:ok, _igniter, repo} -> repo
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
table =
|
||||
case table do
|
||||
{:ok, _igniter, table} -> table
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
{resource, repo, table}
|
||||
end)
|
||||
|
||||
igniter = Igniter.include_all_elixir_files(igniter)
|
||||
|
||||
opts =
|
||||
if opts[:tables] do
|
||||
Keyword.put(
|
||||
opts,
|
||||
:tables,
|
||||
opts[:tables]
|
||||
|> List.wrap()
|
||||
|> Enum.join(",")
|
||||
|> String.split(",")
|
||||
)
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
opts =
|
||||
if opts[:skip_tables] do
|
||||
Keyword.put(
|
||||
opts,
|
||||
:skip_tables,
|
||||
opts[:skip_tables]
|
||||
|> List.wrap()
|
||||
|> Enum.join(",")
|
||||
|> String.split(",")
|
||||
)
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
specs =
|
||||
repos
|
||||
|> Enum.flat_map(&Spec.tables(&1, skip_tables: opts[:skip_tables], tables: opts[:tables]))
|
||||
|> Enum.map(fn %{table_name: table} = spec ->
|
||||
resource =
|
||||
table
|
||||
|> Macro.camelize()
|
||||
|> then(&Module.concat([domain, &1]))
|
||||
|
||||
%{spec | resource: resource}
|
||||
end)
|
||||
|> Enum.group_by(& &1.resource)
|
||||
|> Enum.map(fn
|
||||
{_resource, [single]} ->
|
||||
single
|
||||
|
||||
{resource, specs} ->
|
||||
raise """
|
||||
Duplicate resource names detected across multiple repos: #{inspect(resource)}
|
||||
|
||||
#{inspect(Enum.map(specs, & &1.repo))}
|
||||
|
||||
To address this, run this command separately for each repo and specify the
|
||||
`--domain` option to put the resources into a separate domain, or omit the table
|
||||
with `--tables` or `--skip-tables`
|
||||
"""
|
||||
end)
|
||||
|> Spec.add_relationships(resources)
|
||||
|
||||
Enum.reduce(specs, igniter, fn table_spec, igniter ->
|
||||
table_to_resource(igniter, table_spec, domain, opts)
|
||||
end)
|
||||
end
|
||||
|
||||
defp table_to_resource(
|
||||
igniter,
|
||||
%AshPostgres.ResourceGenerator.Spec{} = table_spec,
|
||||
domain,
|
||||
opts
|
||||
) do
|
||||
no_migrate_flag =
|
||||
if opts[:no_migrations] do
|
||||
"migrate? false"
|
||||
end
|
||||
|
||||
resource =
|
||||
"""
|
||||
use Ash.Resource,
|
||||
domain: #{inspect(domain)},
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
table #{inspect(table_spec.table_name)}
|
||||
repo #{inspect(table_spec.repo)}
|
||||
#{no_migrate_flag}
|
||||
#{references(table_spec, opts[:no_migrations])}
|
||||
#{custom_indexes(table_spec, opts[:no_migrations])}
|
||||
#{check_constraints(table_spec, opts[:no_migrations])}
|
||||
#{skip_unique_indexes(table_spec)}
|
||||
#{identity_index_names(table_spec)}
|
||||
end
|
||||
|
||||
attributes do
|
||||
#{attributes(table_spec)}
|
||||
end
|
||||
"""
|
||||
|> add_identities(table_spec)
|
||||
|> add_relationships(table_spec)
|
||||
|
||||
igniter
|
||||
|> Ash.Domain.Igniter.add_resource_reference(domain, table_spec.resource)
|
||||
|> Igniter.Code.Module.create_module(table_spec.resource, resource)
|
||||
end
|
||||
|
||||
defp check_constraints(%{check_constraints: _check_constraints}, true) do
|
||||
""
|
||||
end
|
||||
|
||||
defp check_constraints(%{check_constraints: []}, _) do
|
||||
""
|
||||
end
|
||||
|
||||
defp check_constraints(%{check_constraints: check_constraints}, _) do
|
||||
IO.inspect(check_constraints)
|
||||
|
||||
check_constraints =
|
||||
Enum.map_join(check_constraints, "\n", fn check_constraint ->
|
||||
"""
|
||||
check_constraint :#{check_constraint.column}, "#{check_constraint.name}", check: "#{check_constraint.expression}", message: "is invalid"
|
||||
"""
|
||||
end)
|
||||
|
||||
"""
|
||||
check_constraints do
|
||||
#{check_constraints}
|
||||
end
|
||||
"""
|
||||
end
|
||||
|
||||
defp skip_unique_indexes(%{indexes: indexes}) do
|
||||
indexes
|
||||
|> Enum.filter(& &1.unique?)
|
||||
|> Enum.filter(fn %{columns: columns} ->
|
||||
Enum.all?(columns, &Regex.match?(~r/^[0-9a-zA-Z_]+$/, &1))
|
||||
end)
|
||||
|> Enum.reject(&index_as_identity?/1)
|
||||
|> case do
|
||||
[] ->
|
||||
""
|
||||
|
||||
indexes ->
|
||||
"""
|
||||
skip_unique_indexes [#{Enum.map_join(indexes, ",", &":#{&1.name}")}]
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
defp identity_index_names(%{indexes: indexes}) do
|
||||
indexes
|
||||
|> Enum.filter(& &1.unique?)
|
||||
|> Enum.filter(fn %{columns: columns} ->
|
||||
Enum.all?(columns, &Regex.match?(~r/^[0-9a-zA-Z_]+$/, &1))
|
||||
end)
|
||||
|> case do
|
||||
[] ->
|
||||
[]
|
||||
|
||||
indexes ->
|
||||
indexes
|
||||
|> Enum.map_join(", ", fn index ->
|
||||
"#{index.name}: \"#{index.name}\""
|
||||
end)
|
||||
|> then(&"identity_index_names [#{&1}]")
|
||||
end
|
||||
end
|
||||
|
||||
defp add_identities(str, %{indexes: indexes}) do
|
||||
indexes
|
||||
|> Enum.filter(& &1.unique?)
|
||||
|> Enum.filter(fn %{columns: columns} ->
|
||||
Enum.all?(columns, &Regex.match?(~r/^[0-9a-zA-Z_]+$/, &1))
|
||||
end)
|
||||
|> Enum.map(fn index ->
|
||||
name = index.name
|
||||
|
||||
fields = "[" <> Enum.map_join(index.columns, ", ", &":#{&1}") <> "]"
|
||||
|
||||
case identity_options(index) do
|
||||
"" ->
|
||||
"identity :#{name}, #{fields}"
|
||||
|
||||
options ->
|
||||
"""
|
||||
identity :#{name}, #{fields} do
|
||||
#{options}
|
||||
end
|
||||
"""
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
[] ->
|
||||
str
|
||||
|
||||
identities ->
|
||||
"""
|
||||
#{str}
|
||||
|
||||
identities do
|
||||
#{Enum.join(identities, "\n")}
|
||||
end
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
defp identity_options(index) do
|
||||
""
|
||||
|> add_identity_where(index)
|
||||
|> add_nils_distinct?(index)
|
||||
end
|
||||
|
||||
defp add_identity_where(str, %{where_clause: nil}), do: str
|
||||
|
||||
defp add_identity_where(str, %{name: name, where_clause: where_clause}) do
|
||||
Logger.warning("""
|
||||
Index #{name} has been left commented out in its resource
|
||||
Manual conversion of `#{where_clause}` to an Ash expression is required.
|
||||
""")
|
||||
|
||||
"""
|
||||
#{str}
|
||||
# Express `#{where_clause}` as an Ash expression
|
||||
# where expr(...)
|
||||
"""
|
||||
end
|
||||
|
||||
defp add_nils_distinct?(str, %{nils_distinct?: false}) do
|
||||
"#{str}\n nils_distinct? false"
|
||||
end
|
||||
|
||||
defp add_nils_distinct?(str, _), do: str
|
||||
|
||||
defp add_relationships(str, %{relationships: []}) do
|
||||
str
|
||||
end
|
||||
|
||||
defp add_relationships(str, %{relationships: relationships} = spec) do
|
||||
relationships
|
||||
|> Enum.map_join("\n", fn relationship ->
|
||||
case relationship_options(spec, relationship) do
|
||||
"" ->
|
||||
"#{relationship.type} :#{relationship.name}, #{inspect(relationship.destination)}"
|
||||
|
||||
options ->
|
||||
"""
|
||||
#{relationship.type} :#{relationship.name}, #{inspect(relationship.destination)} do
|
||||
#{options}
|
||||
end
|
||||
"""
|
||||
end
|
||||
end)
|
||||
|> then(fn rels ->
|
||||
"""
|
||||
#{str}
|
||||
|
||||
relationships do
|
||||
#{rels}
|
||||
end
|
||||
"""
|
||||
end)
|
||||
end
|
||||
|
||||
defp relationship_options(spec, %{type: :belongs_to} = rel) do
|
||||
case Enum.find(spec.attributes, fn attribute ->
|
||||
attribute.name == rel.source_attribute
|
||||
end) do
|
||||
%{
|
||||
default: default,
|
||||
generated?: generated?,
|
||||
source: source,
|
||||
name: name
|
||||
}
|
||||
when not is_nil(default) or generated? or source != name ->
|
||||
"define_attribute? false"
|
||||
|> add_destination_attribute(rel, "id")
|
||||
|> add_source_attribute(rel, "#{rel.name}_id")
|
||||
|> add_allow_nil(rel)
|
||||
|> add_filter(rel)
|
||||
|
||||
attribute ->
|
||||
""
|
||||
|> add_destination_attribute(rel, "id")
|
||||
|> add_source_attribute(rel, "#{rel.name}_id")
|
||||
|> add_allow_nil(rel)
|
||||
|> add_primary_key(attribute.primary_key?)
|
||||
|> add_attribute_type(attribute)
|
||||
|> add_filter(rel)
|
||||
end
|
||||
end
|
||||
|
||||
defp relationship_options(_spec, rel) do
|
||||
default_destination_attribute =
|
||||
rel.source
|
||||
|> Module.split()
|
||||
|> List.last()
|
||||
|> Macro.underscore()
|
||||
|> Kernel.<>("_id")
|
||||
|
||||
""
|
||||
|> add_destination_attribute(rel, default_destination_attribute)
|
||||
|> add_source_attribute(rel, "id")
|
||||
|> add_filter(rel)
|
||||
end
|
||||
|
||||
defp add_filter(str, %{match_with: []}), do: str
|
||||
|
||||
defp add_filter(str, %{match_with: match_with}) do
|
||||
filter =
|
||||
Enum.map_join(match_with, " and ", fn {source, dest} ->
|
||||
"parent(#{source}) == #{dest}"
|
||||
end)
|
||||
|
||||
"#{str}\n filter expr(#{filter})"
|
||||
end
|
||||
|
||||
defp add_attribute_type(str, %{attr_type: :uuid}), do: str
|
||||
|
||||
defp add_attribute_type(str, %{attr_type: attr_type}) do
|
||||
"#{str}\n attribute_type :#{attr_type}"
|
||||
end
|
||||
|
||||
defp add_destination_attribute(str, rel, default) do
|
||||
if rel.destination_attribute == default do
|
||||
str
|
||||
else
|
||||
"#{str}\n destination_attribute :#{rel.destination_attribute}"
|
||||
end
|
||||
end
|
||||
|
||||
defp add_source_attribute(str, rel, default) do
|
||||
if rel.source_attribute == default do
|
||||
str
|
||||
else
|
||||
"#{str}\n source_attribute :#{rel.source_attribute}"
|
||||
end
|
||||
end
|
||||
|
||||
defp references(_table_spec, true) do
|
||||
""
|
||||
end
|
||||
|
||||
defp references(table_spec, _) do
|
||||
table_spec.foreign_keys
|
||||
|> Enum.flat_map(fn %Spec.ForeignKey{} = foreign_key ->
|
||||
default_name = "#{table_spec.table_name}_#{foreign_key.column}_fkey"
|
||||
|
||||
if default_name == foreign_key.constraint_name and
|
||||
foreign_key.on_update == "NO ACTION" and
|
||||
foreign_key.on_delete == "NO ACTION" and
|
||||
foreign_key.match_type in ["SIMPLE", "NONE"] do
|
||||
[]
|
||||
else
|
||||
relationship =
|
||||
Enum.find(table_spec.relationships, fn relationship ->
|
||||
relationship.type == :belongs_to and
|
||||
relationship.constraint_name == foreign_key.constraint_name
|
||||
end).name
|
||||
|
||||
options =
|
||||
""
|
||||
|> add_on(:update, foreign_key.on_update)
|
||||
|> add_on(:delete, foreign_key.on_delete)
|
||||
|> add_match_with(foreign_key.match_with)
|
||||
|> add_match_type(foreign_key.match_type)
|
||||
|
||||
[
|
||||
"""
|
||||
reference :#{relationship} do
|
||||
#{options}
|
||||
end
|
||||
"""
|
||||
]
|
||||
end
|
||||
|> Enum.join("\n")
|
||||
|> String.trim()
|
||||
|> then(
|
||||
&[
|
||||
"""
|
||||
references do
|
||||
#{&1}
|
||||
end
|
||||
"""
|
||||
]
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_match_with(str, empty) when empty in [[], nil], do: str
|
||||
|
||||
defp add_match_with(str, keyval),
|
||||
do: str <> "\nmatch_with [#{Enum.map_join(keyval, fn {key, val} -> "#{key}: :#{val}" end)}]"
|
||||
|
||||
defp add_match_type(str, type) when type in ["SIMPLE", "NONE"], do: str
|
||||
|
||||
defp add_match_type(str, "FULL"), do: str <> "\nmatch_type :full"
|
||||
defp add_match_type(str, "PARTIAL"), do: str <> "\nmatch_type :partial"
|
||||
|
||||
defp add_on(str, type, "RESTRICT"), do: str <> "\non_#{type} :restrict"
|
||||
defp add_on(str, type, "CASCADE"), do: str <> "\non_#{type} :#{type}"
|
||||
defp add_on(str, type, "SET NULL"), do: str <> "\non_#{type} :nilify"
|
||||
defp add_on(str, _type, _), do: str
|
||||
|
||||
defp custom_indexes(table_spec, true) do
|
||||
table_spec.indexes
|
||||
|> Enum.reject(fn index ->
|
||||
!index.unique? || (&index_as_identity?/1)
|
||||
end)
|
||||
|> Enum.reject(fn index ->
|
||||
Enum.any?(index.columns, &String.contains?(&1, "("))
|
||||
end)
|
||||
|> case do
|
||||
[] ->
|
||||
""
|
||||
|
||||
indexes ->
|
||||
indexes
|
||||
|> Enum.map_join(", ", fn %{index: name, columns: columns} ->
|
||||
columns = Enum.map_join(columns, ", ", &":#{&1}")
|
||||
"{[#{columns}], #{inspect(name)}}"
|
||||
end)
|
||||
|> then(fn index_names ->
|
||||
"unique_index_names [#{index_names}]"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp custom_indexes(table_spec, _) do
|
||||
table_spec.indexes
|
||||
|> Enum.reject(&index_as_identity?/1)
|
||||
|> case do
|
||||
[] ->
|
||||
""
|
||||
|
||||
indexes ->
|
||||
indexes
|
||||
|> Enum.map_join("\n", fn index ->
|
||||
columns =
|
||||
index.columns
|
||||
|> Enum.map_join(", ", fn thing ->
|
||||
if String.contains?(thing, "(") do
|
||||
inspect(thing)
|
||||
else
|
||||
":#{thing}"
|
||||
end
|
||||
end)
|
||||
|
||||
case index_options(table_spec, index) do
|
||||
"" ->
|
||||
"index [#{columns}]"
|
||||
|
||||
options ->
|
||||
"""
|
||||
index [#{columns}] do
|
||||
#{options}
|
||||
end
|
||||
"""
|
||||
end
|
||||
end)
|
||||
|> then(fn indexes ->
|
||||
"""
|
||||
custom_indexes do
|
||||
#{indexes}
|
||||
end
|
||||
"""
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp index_as_identity?(index) do
|
||||
is_nil(index.where_clause) and index.using == "btree" and index.include in [nil, []] and
|
||||
Enum.all?(index.columns, &Regex.match?(~r/^[0-9a-zA-Z_]+$/, &1))
|
||||
end
|
||||
|
||||
defp index_options(spec, index) do
|
||||
default_name =
|
||||
if Enum.all?(index.columns, &Regex.match?(~r/^[0-9a-zA-Z_]+$/, &1)) do
|
||||
AshPostgres.CustomIndex.name(spec.table_name, %{fields: index.columns})
|
||||
end
|
||||
|
||||
""
|
||||
|> add_index_name(index.name, default_name)
|
||||
|> add_unique(index.unique?)
|
||||
|> add_using(index.using)
|
||||
|> add_where(index.where_clause)
|
||||
|> add_include(index.include)
|
||||
|> add_nulls_distinct(index.nulls_distinct)
|
||||
end
|
||||
|
||||
defp add_index_name(str, default, default), do: str
|
||||
defp add_index_name(str, name, _), do: str <> "\nname #{inspect(name)}"
|
||||
|
||||
defp add_unique(str, false), do: str
|
||||
defp add_unique(str, true), do: str <> "\nunique true"
|
||||
|
||||
defp add_nulls_distinct(str, true), do: str
|
||||
defp add_nulls_distinct(str, false), do: str <> "\nnulls_distinct false"
|
||||
|
||||
defp add_using(str, "btree"), do: str
|
||||
defp add_using(str, using), do: str <> "\nusing #{inspect(using)}"
|
||||
|
||||
defp add_where(str, empty) when empty in [nil, ""], do: str
|
||||
defp add_where(str, where), do: str <> "\nwhere #{inspect(where)}"
|
||||
|
||||
defp add_include(str, empty) when empty in [nil, []], do: str
|
||||
|
||||
defp add_include(str, include),
|
||||
do: str <> "\ninclude [#{Enum.map_join(include, ", ", &inspect/1)}]"
|
||||
|
||||
defp attributes(table_spec) do
|
||||
table_spec.attributes
|
||||
|> Enum.split_with(& &1.default)
|
||||
|> then(fn {l, r} -> r ++ l end)
|
||||
|> Enum.split_with(& &1.primary_key?)
|
||||
|> then(fn {l, r} -> l ++ r end)
|
||||
|> Enum.filter(fn attribute ->
|
||||
if not is_nil(attribute.default) or !!attribute.generated? or
|
||||
attribute.source != attribute.name do
|
||||
true
|
||||
else
|
||||
not Enum.any?(table_spec.relationships, fn relationship ->
|
||||
relationship.type == :belongs_to and relationship.source_attribute == attribute.name
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|> Enum.map_join("\n", &attribute(&1))
|
||||
end
|
||||
|
||||
defp attribute(attribute) do
|
||||
now_default = &DateTime.utc_now/0
|
||||
uuid_default = &Ash.UUID.generate/0
|
||||
|
||||
{constructor, attribute, type?, type_option?} =
|
||||
case attribute do
|
||||
%{name: "updated_at", attr_type: attr_type} ->
|
||||
{"update_timestamp", %{attribute | default: nil, generated?: false}, false,
|
||||
attr_type != :utc_datetime_usec}
|
||||
|
||||
%{default: default, attr_type: attr_type}
|
||||
when default == now_default ->
|
||||
{"create_timestamp", %{attribute | default: nil, generated?: false}, false,
|
||||
attr_type != :utc_datetime_usec}
|
||||
|
||||
%{default: default, attr_type: attr_type, primary_key?: true}
|
||||
when default == uuid_default ->
|
||||
{"uuid_primary_key",
|
||||
%{attribute | default: nil, primary_key?: false, generated?: false, allow_nil?: true},
|
||||
false, attr_type != :uuid}
|
||||
|
||||
_ ->
|
||||
{"attribute", attribute, true, false}
|
||||
end
|
||||
|
||||
case String.trim(options(attribute, type_option?)) do
|
||||
"" ->
|
||||
if type? do
|
||||
"#{constructor} :#{attribute.name}, #{inspect(attribute.attr_type)}"
|
||||
else
|
||||
"#{constructor} :#{attribute.name}"
|
||||
end
|
||||
|
||||
options ->
|
||||
if type? do
|
||||
"""
|
||||
#{constructor} :#{attribute.name}, #{inspect(attribute.attr_type)} do
|
||||
#{options}
|
||||
end
|
||||
"""
|
||||
else
|
||||
"""
|
||||
#{constructor} :#{attribute.name} do
|
||||
#{options}
|
||||
end
|
||||
"""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp options(attribute, type_option?) do
|
||||
""
|
||||
|> add_primary_key(attribute)
|
||||
|> add_allow_nil(attribute)
|
||||
|> add_sensitive(attribute)
|
||||
|> add_default(attribute)
|
||||
|> add_type(attribute, type_option?)
|
||||
|> add_generated(attribute)
|
||||
|> add_source(attribute)
|
||||
end
|
||||
|
||||
defp add_type(str, %{attr_type: attr_type}, true) do
|
||||
str <> "\n type #{inspect(attr_type)}"
|
||||
end
|
||||
|
||||
defp add_type(str, _, _), do: str
|
||||
|
||||
defp add_generated(str, %{generated?: true}) do
|
||||
str <> "\n generated? true"
|
||||
end
|
||||
|
||||
defp add_generated(str, _), do: str
|
||||
|
||||
defp add_source(str, %{name: name, source: source}) when name != source do
|
||||
str <> "\n source :#{source}"
|
||||
end
|
||||
|
||||
defp add_source(str, _), do: str
|
||||
|
||||
defp add_primary_key(str, %{primary_key?: true}) do
|
||||
str <> "\n primary_key? true"
|
||||
end
|
||||
|
||||
defp add_primary_key(str, _), do: str
|
||||
|
||||
defp add_allow_nil(str, %{allow_nil?: false}) do
|
||||
str <> "\n allow_nil? false"
|
||||
end
|
||||
|
||||
defp add_allow_nil(str, _), do: str
|
||||
|
||||
defp add_sensitive(str, %{sensitive?: true}) do
|
||||
str <> "\n sensitive? true"
|
||||
end
|
||||
|
||||
defp add_sensitive(str, _), do: str
|
||||
|
||||
defp add_default(str, %{default: default}) when not is_nil(default) do
|
||||
str <> "\n default #{inspect(default)}"
|
||||
end
|
||||
|
||||
defp add_default(str, _), do: str
|
||||
end
|
73
lib/resource_generator/sensitive_data.ex
Normal file
73
lib/resource_generator/sensitive_data.ex
Normal file
|
@ -0,0 +1,73 @@
|
|||
defmodule AshPostgres.ResourceGenerator.SensitiveData do
|
||||
# I got this from ChatGPT, but this is a best effort transformation
|
||||
# anyway.
|
||||
@sensitive_patterns [
|
||||
# Password-related
|
||||
~r/password/i,
|
||||
~r/passwd/i,
|
||||
~r/pass/i,
|
||||
~r/pwd/i,
|
||||
~r/hash(ed)?(_password)?/i,
|
||||
|
||||
# Authentication-related
|
||||
~r/auth(_key)?/i,
|
||||
~r/token/i,
|
||||
~r/secret(_key)?/i,
|
||||
~r/api_key/i,
|
||||
|
||||
# Personal Information
|
||||
~r/ssn/i,
|
||||
~r/social(_security)?(_number)?/i,
|
||||
~r/(credit_?card|cc)(_number)?/i,
|
||||
~r/passport(_number)?/i,
|
||||
~r/driver_?licen(s|c)e(_number)?/i,
|
||||
~r/national_id/i,
|
||||
|
||||
# Financial Information
|
||||
~r/account(_number)?/i,
|
||||
~r/routing(_number)?/i,
|
||||
~r/iban/i,
|
||||
~r/swift(_code)?/i,
|
||||
~r/tax_id/i,
|
||||
|
||||
# Contact Information
|
||||
~r/phone(_number)?/i,
|
||||
~r/email(_address)?/i,
|
||||
~r/address/i,
|
||||
|
||||
# Health Information
|
||||
~r/medical(_record)?/i,
|
||||
~r/health(_data)?/i,
|
||||
~r/diagnosis/i,
|
||||
~r/treatment/i,
|
||||
|
||||
# Biometric Data
|
||||
~r/fingerprint/i,
|
||||
~r/retina_scan/i,
|
||||
~r/face_id/i,
|
||||
~r/dna/i,
|
||||
|
||||
# Encrypted or Encoded Data
|
||||
~r/encrypt(ed)?/i,
|
||||
~r/encoded/i,
|
||||
~r/cipher/i,
|
||||
|
||||
# Other Potentially Sensitive Data
|
||||
~r/private(_key)?/i,
|
||||
~r/confidential/i,
|
||||
~r/restricted/i,
|
||||
~r/sensitive/i,
|
||||
|
||||
# General patterns
|
||||
~r/.*_salt/i,
|
||||
~r/.*_secret/i,
|
||||
~r/.*_key/i,
|
||||
~r/.*_token/i
|
||||
]
|
||||
|
||||
def is_sensitive?(column_name) do
|
||||
Enum.any?(@sensitive_patterns, fn pattern ->
|
||||
Regex.match?(pattern, column_name)
|
||||
end)
|
||||
end
|
||||
end
|
1026
lib/resource_generator/spec.ex
Normal file
1026
lib/resource_generator/spec.ex
Normal file
File diff suppressed because it is too large
Load diff
2
mix.exs
2
mix.exs
|
@ -169,6 +169,8 @@ defmodule AshPostgres.MixProject do
|
|||
{:ecto, "~> 3.12 and >= 3.12.1"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:inflex, "~> 2.1"},
|
||||
{:owl, "~> 0.11"},
|
||||
# dev/test dependencies
|
||||
{:eflame, "~> 1.0", only: [:dev, :test]},
|
||||
{:simple_sat, "~> 0.1", only: [:dev, :test]},
|
||||
|
|
12
mix.lock
12
mix.lock
|
@ -8,15 +8,15 @@
|
|||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
|
||||
"ecto": {:hex, :ecto, "3.12.2", "bae2094f038e9664ce5f089e5f3b6132a535d8b018bd280a485c2f33df5c0ce1", [: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", "492e67c70f3a71c6afe80d946d3ced52ecc57c53c9829791bfff1830ff5a1f0c"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
|
||||
"eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"},
|
||||
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
|
||||
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
|
||||
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "a663c13478a49d29ae0267b6e45badb803267cf0", []},
|
||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "d571628fd829a510d219bcb7162400baff50977f", []},
|
||||
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
|
||||
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
|
||||
"git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"},
|
||||
|
@ -25,9 +25,9 @@
|
|||
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||
"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.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
|
||||
"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"},
|
||||
"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"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "F24673A4219DEC6873571CCF68B8F0CC34B5843DAA2D7B71A16EFE576C385C1C",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.AshPostgres.TestRepo",
|
||||
"schema": null,
|
||||
"table": "schematic_groups"
|
||||
}
|
|
@ -21,6 +21,15 @@ defmodule AshPostgres.Test.Domain do
|
|||
resource(AshPostgres.Test.Record)
|
||||
resource(AshPostgres.Test.PostFollower)
|
||||
resource(AshPostgres.Test.StatefulPostFollower)
|
||||
resource(CalcDependency.Dependency)
|
||||
resource(CalcDependency.Element)
|
||||
resource(CalcDependency.ElementContext)
|
||||
resource(CalcDependency.Location)
|
||||
resource(CalcDependency.Operation)
|
||||
resource(CalcDependency.OperationVersion)
|
||||
resource(CalcDependency.SchematicGroup)
|
||||
resource(CalcDependency.Segment)
|
||||
resource(CalcDependency.Verb)
|
||||
end
|
||||
|
||||
authorization do
|
||||
|
|
Loading…
Reference in a new issue