feat: mix ash_postgres.gen.resources

This commit is contained in:
Zach Daniel 2024-09-04 20:28:45 -04:00
parent 0ccb35a713
commit 133be5094b
12 changed files with 2002 additions and 8 deletions

View 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.

View file

@ -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

View file

@ -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

View 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

View file

@ -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.

View 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

View 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

File diff suppressed because it is too large Load diff

View file

@ -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]},

View file

@ -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"},

View file

@ -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"
}

View file

@ -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