improvement: get a build set up

improvement: fix lint/security issues
improvement: add CSP
improvement: remove currently unnecessary/old code
This commit is contained in:
Zach Daniel 2022-08-06 19:22:58 -04:00
parent 59e9f1e8df
commit 025b56d1a4
78 changed files with 1037 additions and 632 deletions

19
.check.exs Normal file
View file

@ -0,0 +1,19 @@
[
## all available options with default values (see `mix check` docs for description)
# parallel: true,
# skipped: true,
## list of tools (see `mix check` docs for defaults)
tools: [
## curated tools may be disabled (e.g. the check for compilation warnings)
# {:compiler, false},
## ...or adjusted (e.g. use one-line formatter for more compact credo output)
# {:credo, "mix credo --format oneline"},
## custom new tools may be added (mix tasks or arbitrary commands)
# {:my_mix_task, command: "mix release", env: %{"MIX_ENV" => "prod"}},
# {:my_arbitrary_tool, command: "npm test", cd: "assets"},
# {:my_arbitrary_script, command: ["my_script", "argument with spaces"], cd: "scripts"}
{:npm_test, false}
]
]

209
.credo.exs Normal file
View file

@ -0,0 +1,209 @@
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any config using `mix credo -C <name>`. If no config name is given
# "default" is used.
#
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
#
included: [
"lib/",
"src/",
"test/",
"web/",
"apps/*/lib/",
"apps/*/src/",
"apps/*/test/",
"apps/*/web/"
],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
},
#
# Load and configure plugins here:
#
plugins: [],
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
#
requires: [],
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
#
strict: false,
#
# To modify the timeout for parsing files, change this value:
#
parse_timeout: 5000,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
#
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: %{
enabled: [
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage, false},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
{Credo.Check.Design.TagFIXME, []},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, false},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, [max_nesting: 4]},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.RedundantWithClauseResult, []},
#
## Warnings
#
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.SpecWithStruct, []},
{Credo.Check.Warning.WrongTestFileExtension, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
{Credo.Check.Warning.UnsafeExec, []}
],
disabled: [
#
# Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`)
#
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
# and be sure to use `mix credo --strict` to see low priority checks)
#
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
{Credo.Check.Consistency.UnusedVariableNames, []},
{Credo.Check.Design.DuplicatedCode, []},
{Credo.Check.Design.SkipTestWithoutComment, []},
{Credo.Check.Readability.AliasAs, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.ImplTrue, []},
{Credo.Check.Readability.MultiAlias, []},
{Credo.Check.Readability.NestedFunctionCalls, []},
{Credo.Check.Readability.SeparateAliasRequire, []},
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
{Credo.Check.Readability.Specs, []},
{Credo.Check.Readability.StrictModuleLayout, []},
{Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Refactor.ABCSize, []},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.FilterReject, []},
{Credo.Check.Refactor.IoPuts, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.ModuleDependencies, []},
{Credo.Check.Refactor.NegatedIsNil, []},
{Credo.Check.Refactor.PipeChainStart, []},
{Credo.Check.Refactor.RejectFilter, []},
{Credo.Check.Refactor.VariableRebinding, []},
{Credo.Check.Warning.LazyLogging, []},
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}
# {Credo.Check.Refactor.MapInto, []},
#
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
}
]
}

View file

@ -11,6 +11,7 @@
render_attributes: 1, render_attributes: 1,
use_path_for_name?: 1, use_path_for_name?: 1,
sanitized_name_attribute: 1, sanitized_name_attribute: 1,
show_docs_on: 1 show_docs_on: 1,
header_ids?: 1
] ]
] ]

View file

@ -68,3 +68,9 @@ config :phoenix, :json_library, Jason
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"
config :plug_content_security_policy,
nonces_for: [:script_src],
directives: %{
img_src: ~w('self' data data:)
}

View file

@ -83,3 +83,6 @@ config :phoenix, :stacktrace_depth, 20
config :phoenix, :plug_init_mode, :runtime config :phoenix, :plug_init_mode, :runtime
config :ash_hq, AshHq.Mailer, adapter: Swoosh.Adapters.Local config :ash_hq, AshHq.Mailer, adapter: Swoosh.Adapters.Local
config :plug_content_security_policy,
report_only: true

View file

@ -52,3 +52,9 @@ config :logger, level: :info
config :ash_hq, AshHq.Mailer, adapter: Swoosh.Adapters.Postmark config :ash_hq, AshHq.Mailer, adapter: Swoosh.Adapters.Postmark
config :swoosh, :api_client, Swoosh.ApiClient.Finch config :swoosh, :api_client, Swoosh.ApiClient.Finch
config :plug_content_security_policy,
nonces_for: [:script_src],
directives: %{
img_src: ~w('self' data data:)
}

View file

@ -30,3 +30,6 @@ config :logger, level: :warn
config :phoenix, :plug_init_mode, :runtime config :phoenix, :plug_init_mode, :runtime
config :ash_hq, AshHq.Mailer, adapter: Swoosh.Adapters.Local config :ash_hq, AshHq.Mailer, adapter: Swoosh.Adapters.Local
config :plug_content_security_policy,
report_only: true

View file

@ -1,3 +1,6 @@
defmodule AshHq.Accounts do defmodule AshHq.Accounts do
@moduledoc """
Handles user and user token related operations/state
"""
use Ash.Api, otp_app: :ash_hq use Ash.Api, otp_app: :ash_hq
end end

View file

@ -1,4 +1,8 @@
defmodule AshHq.Accounts.EmailNotifier do defmodule AshHq.Accounts.EmailNotifier do
@moduledoc """
Hooks into resource notifications on the user token resource to send emails
"""
def notify(%Ash.Notifier.Notification{ def notify(%Ash.Notifier.Notification{
resource: AshHq.Accounts.UserToken, resource: AshHq.Accounts.UserToken,
action: %{name: :build_email_token}, action: %{name: :build_email_token},

View file

@ -1,4 +1,8 @@
defmodule AshHq.Accounts.Emails do defmodule AshHq.Accounts.Emails do
@moduledoc """
Delivers emails.
"""
import Swoosh.Email import Swoosh.Email
def deliver_confirmation_instructions(user, url) do def deliver_confirmation_instructions(user, url) do

View file

@ -1,9 +1,10 @@
defmodule AshHq.Accounts.Preparations.DetermineDaysForToken do defmodule AshHq.Accounts.Preparations.DetermineDaysForToken do
use Ash.Resource.Preparation @moduledoc """
Sets a `days_for_token` context on the query.
def determine_days_for_token() do This corresponds to how many days the token should be considered valid. See `AshHq.Accounts.User.Helpers` for more.
{__MODULE__, []} """
end use Ash.Resource.Preparation
def prepare(query, _opts, _) do def prepare(query, _opts, _) do
Ash.Query.put_context( Ash.Query.put_context(

View file

@ -1,4 +1,8 @@
defmodule AshHq.Accounts.Preparations.SetHashedToken do defmodule AshHq.Accounts.Preparations.SetHashedToken do
@moduledoc """
Takes a provided token and hashes it, setting it as the context `hashed_token`
"""
use Ash.Resource.Preparation use Ash.Resource.Preparation
@hash_algorithm :sha256 @hash_algorithm :sha256

View file

@ -1,4 +1,6 @@
defmodule AshHq.Accounts.Registry do defmodule AshHq.Accounts.Registry do
@moduledoc false
use Ash.Registry, use Ash.Registry,
extensions: [Ash.Registry.ResourceValidations] extensions: [Ash.Registry.ResourceValidations]

View file

@ -1,4 +1,9 @@
defmodule AshHq.Accounts.User.Changes.RemoveAllTokens do defmodule AshHq.Accounts.User.Changes.RemoveAllTokens do
@moduledoc """
Removes all tokens for a given user.
Since Ash does not yet support bulk actions, this goes straight to the data layer.
"""
use Ash.Resource.Change use Ash.Resource.Change
require Ash.Query require Ash.Query

View file

@ -1,4 +1,5 @@
defmodule AshHq.Accounts.User.Helpers do defmodule AshHq.Accounts.User.Helpers do
@moduledoc "Contains values used in various places for authentication"
@reset_password_validity_in_days 1 @reset_password_validity_in_days 1
@confirm_validity_in_days 7 @confirm_validity_in_days 7
@change_email_validity_in_days 7 @change_email_validity_in_days 7

View file

@ -1,28 +0,0 @@
defmodule AshHq.Accounts.User.Preparations.DecodeToken do
use Ash.Resource.Preparation
alias Ash.Error.Query.InvalidArgument
def prepare(query, _opts, _) do
case Ash.Query.get_argument(query, :token) do
nil ->
query
token ->
case Base.url_decode64(token, padding: false) do
{:ok, decoded} ->
Ash.Query.set_argument(
query,
:token,
decoded
)
:error ->
Ash.Query.add_error(
query,
InvalidArgument.exception(field: :token, message: "could not be decoded")
)
end
end
end
end

View file

@ -1,4 +1,10 @@
defmodule AshHq.Accounts.User.Preparations.ValidatePassword do defmodule AshHq.Accounts.User.Preparations.ValidatePassword do
@moduledoc """
Given the result of a query for users, and a password argument, ensures that the `password` is valid.
If there is more or less than one result, or if the password is invalid, then this removes the results of the query.
In this way, you can't tell from the outside wether or not the password was invalid or there was no matching account.
"""
use Ash.Resource.Preparation use Ash.Resource.Preparation
def prepare(query, _opts, _) do def prepare(query, _opts, _) do

View file

@ -1,21 +1,9 @@
defmodule AshHq.Accounts.User do defmodule AshHq.Accounts.User do
use Ash.Resource, @moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer data_layer: AshPostgres.DataLayer
alias AshHq.Accounts.Preparations, warn: false
alias AshHq.Accounts.User.Preparations, as: UserPreparations, warn: false
alias AshHq.Accounts.User.Changes, warn: false
alias AshHq.Accounts.User.Validations, warn: false
identities do
identity :unique_email, [:email]
end
postgres do
table "users"
repo AshHq.Repo
end
actions do actions do
defaults [:read] defaults [:read]
@ -23,7 +11,7 @@ defmodule AshHq.Accounts.User do
argument :email, :string, allow_nil?: false, sensitive?: true argument :email, :string, allow_nil?: false, sensitive?: true
argument :password, :string, allow_nil?: false, sensitive?: true argument :password, :string, allow_nil?: false, sensitive?: true
prepare UserPreparations.ValidatePassword prepare AshHq.Accounts.User.Preparations.ValidatePassword
filter expr(email == ^arg(:email)) filter expr(email == ^arg(:email))
end end
@ -31,7 +19,7 @@ defmodule AshHq.Accounts.User do
read :by_token do read :by_token do
argument :token, :url_encoded_binary, allow_nil?: false argument :token, :url_encoded_binary, allow_nil?: false
argument :context, :string, allow_nil?: false argument :context, :string, allow_nil?: false
prepare Preparations.DetermineDaysForToken prepare AshHq.Accounts.Preparations.DetermineDaysForToken
filter expr( filter expr(
token.token == ^arg(:token) and token.context == ^arg(:context) and token.token == ^arg(:token) and token.context == ^arg(:context) and
@ -43,8 +31,8 @@ defmodule AshHq.Accounts.User do
argument :token, :url_encoded_binary, allow_nil?: false argument :token, :url_encoded_binary, allow_nil?: false
argument :context, :string, allow_nil?: false argument :context, :string, allow_nil?: false
prepare Preparations.SetHashedToken prepare AshHq.Accounts.Preparations.SetHashedToken
prepare Preparations.DetermineDaysForToken prepare AshHq.Accounts.Preparations.DetermineDaysForToken
filter expr( filter expr(
token.created_at > ago(^context(:days_for_token), :day) and token.created_at > ago(^context(:days_for_token), :day) and
@ -63,7 +51,7 @@ defmodule AshHq.Accounts.User do
min_length: 12 min_length: 12
] ]
change Changes.HashPassword change AshHq.Accounts.User.Changes.HashPassword
end end
update :deliver_user_confirmation_instructions do update :deliver_user_confirmation_instructions do
@ -74,7 +62,7 @@ defmodule AshHq.Accounts.User do
end end
validate attribute_equals(:confirmed_at, nil), message: "already confirmed" validate attribute_equals(:confirmed_at, nil), message: "already confirmed"
change Changes.CreateEmailConfirmationToken change AshHq.Accounts.User.Changes.CreateEmailConfirmationToken
end end
update :deliver_update_email_instructions do update :deliver_update_email_instructions do
@ -86,11 +74,11 @@ defmodule AshHq.Accounts.User do
constraints arity: 1 constraints arity: 1
end end
validate Validations.ValidateCurrentPassword validate AshHq.Accounts.User.Validations.ValidateCurrentPassword
validate changing(:email) validate changing(:email)
change prevent_change(:email) change prevent_change(:email)
change Changes.CreateEmailUpdateToken change AshHq.Accounts.User.Changes.CreateEmailUpdateToken
end end
update :deliver_user_reset_password_instructions do update :deliver_user_reset_password_instructions do
@ -100,21 +88,21 @@ defmodule AshHq.Accounts.User do
constraints arity: 1 constraints arity: 1
end end
change Changes.CreateResetPasswordToken change AshHq.Accounts.User.Changes.CreateResetPasswordToken
end end
update :logout do update :logout do
accept [] accept []
change Changes.RemoveAllTokens change AshHq.Accounts.User.Changes.RemoveAllTokens
end end
update :change_email do update :change_email do
accept [] accept []
argument :token, :url_encoded_binary argument :token, :url_encoded_binary
change Changes.GetEmailFromToken change AshHq.Accounts.User.Changes.GetEmailFromToken
change Changes.DeleteEmailChangeTokens change AshHq.Accounts.User.Changes.DeleteEmailChangeTokens
end end
update :change_password do update :change_password do
@ -131,10 +119,10 @@ defmodule AshHq.Accounts.User do
argument :current_password, :string argument :current_password, :string
validate confirm(:password, :password_confirmation) validate confirm(:password, :password_confirmation)
validate Validations.ValidateCurrentPassword validate AshHq.Accounts.User.Validations.ValidateCurrentPassword
change Changes.HashPassword change AshHq.Accounts.User.Changes.HashPassword
change Changes.RemoveAllTokens change AshHq.Accounts.User.Changes.RemoveAllTokens
end end
update :confirm do update :confirm do
@ -142,7 +130,7 @@ defmodule AshHq.Accounts.User do
argument :delete_confirm_tokens, :boolean, default: false argument :delete_confirm_tokens, :boolean, default: false
change set_attribute(:confirmed_at, &DateTime.utc_now/0) change set_attribute(:confirmed_at, &DateTime.utc_now/0)
change Changes.DeleteConfirmTokens change AshHq.Accounts.User.Changes.DeleteConfirmTokens
end end
end end
@ -162,12 +150,27 @@ defmodule AshHq.Accounts.User do
update_timestamp :updated_at update_timestamp :updated_at
end end
identities do
identity :unique_email, [:email]
end
postgres do
table "users"
repo AshHq.Repo
end
relationships do relationships do
has_one :token, AshHq.Accounts.UserToken, has_one :token, AshHq.Accounts.UserToken,
destination_field: :user_id, destination_field: :user_id,
private?: true private?: true
end end
resource do
description """
Represents the user of a system.
"""
end
validations do validations do
validate match(:email, ~r/^[^\s]+@[^\s]+$/, "must have the @ sign and no spaces") validate match(:email, ~r/^[^\s]+@[^\s]+$/, "must have the @ sign and no spaces")
end end

View file

@ -1,4 +1,10 @@
defmodule AshHq.Accounts.User.Validations.ValidateCurrentPassword do defmodule AshHq.Accounts.User.Validations.ValidateCurrentPassword do
@moduledoc """
Confirms that the provided password is valid.
This is useful for actions that should only be able to be taken on a given user if you know
their password (like changing the email, for example).
"""
use Ash.Resource.Validation use Ash.Resource.Validation
@impl true @impl true

View file

@ -1,7 +0,0 @@
defmodule AshHq.Accounts.User.Validations do
alias AshHq.Accounts.User.Validations
def validate_current_password() do
{Validations.ValidateCurrentPassword, []}
end
end

View file

@ -6,10 +6,6 @@ defmodule AshHq.Accounts.UserToken.Changes.BuildHashedToken do
@rand_size 32 @rand_size 32
@hash_algorithm :sha256 @hash_algorithm :sha256
def build_hashed_token() do
{__MODULE__, []}
end
def change(changeset, _opts, _context) do def change(changeset, _opts, _context) do
token = :crypto.strong_rand_bytes(@rand_size) token = :crypto.strong_rand_bytes(@rand_size)

View file

@ -4,10 +4,6 @@ defmodule AshHq.Accounts.UserToken.Changes.BuildSessionToken do
use Ash.Resource.Change use Ash.Resource.Change
@rand_size 32 @rand_size 32
def build_session_token() do
{__MODULE__, []}
end
def change(changeset, _opts, _context) do def change(changeset, _opts, _context) do
token = :crypto.strong_rand_bytes(@rand_size) token = :crypto.strong_rand_bytes(@rand_size)

View file

@ -1,32 +1,18 @@
defmodule AshHq.Accounts.UserToken do defmodule AshHq.Accounts.UserToken do
use Ash.Resource, @moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
notifiers: [AshHq.Accounts.EmailNotifier] notifiers: [AshHq.Accounts.EmailNotifier]
alias AshHq.Accounts.UserToken.Changes, warn: false
alias AshHq.Accounts.Preparations, warn: false
postgres do
table "user_tokens"
repo AshHq.Repo
references do
reference :user, on_delete: :delete, on_update: :update
end
end
identities do
identity :token_context, [:context, :token]
end
actions do actions do
defaults [:read] defaults [:read]
read :verify_email_token do read :verify_email_token do
argument :token, :url_encoded_binary, allow_nil?: false argument :token, :url_encoded_binary, allow_nil?: false
argument :context, :string, allow_nil?: false argument :context, :string, allow_nil?: false
prepare Preparations.SetHashedToken prepare AshHq.Accounts.Preparations.SetHashedToken
prepare Preparations.DetermineDaysForToken prepare AshHq.Accounts.Preparations.DetermineDaysForToken
filter expr( filter expr(
token == ^context(:hashed_token) and context == ^arg(:context) and token == ^context(:hashed_token) and context == ^arg(:context) and
@ -41,7 +27,7 @@ defmodule AshHq.Accounts.UserToken do
change manage_relationship(:user, type: :replace) change manage_relationship(:user, type: :replace)
change set_attribute(:context, "session") change set_attribute(:context, "session")
change Changes.BuildSessionToken change AshHq.Accounts.UserToken.Changes.BuildSessionToken
end end
create :build_email_token do create :build_email_token do
@ -50,7 +36,7 @@ defmodule AshHq.Accounts.UserToken do
argument :user, :map argument :user, :map
change manage_relationship(:user, type: :replace) change manage_relationship(:user, type: :replace)
change Changes.BuildHashedToken change AshHq.Accounts.UserToken.Changes.BuildHashedToken
end end
end end
@ -64,7 +50,26 @@ defmodule AshHq.Accounts.UserToken do
create_timestamp :created_at create_timestamp :created_at
end end
identities do
identity :token_context, [:context, :token]
end
postgres do
table "user_tokens"
repo AshHq.Repo
references do
reference :user, on_delete: :delete, on_update: :update
end
end
relationships do relationships do
belongs_to :user, AshHq.Accounts.User belongs_to :user, AshHq.Accounts.User
end end
resource do
description """
Represents a token allowing a user to log in, reset their password, or confirm their email.
"""
end
end end

View file

@ -1,4 +1,8 @@
defmodule AshHq.Docs.Changes.AddArgToRelationship do defmodule AshHq.Docs.Changes.AddArgToRelationship do
@moduledoc """
A general utility to pass an argument of the current action down to a relationship change
that is being made.
"""
use Ash.Resource.Change use Ash.Resource.Change
def change(changeset, opts, _) do def change(changeset, opts, _) do

View file

@ -1,4 +1,7 @@
defmodule AshHq.Docs do defmodule AshHq.Docs do
@moduledoc """
Handles documentation data.
"""
use Ash.Api, otp_app: :ash_hq use Ash.Api, otp_app: :ash_hq
execution do execution do

View file

@ -1,4 +1,8 @@
defmodule AshHq.Docs.Extensions.RenderMarkdown.Changes.RenderMarkdown do defmodule AshHq.Docs.Extensions.RenderMarkdown.Changes.RenderMarkdown do
@moduledoc """
Writes a markdown text attribute to its corresponding html attribute.
"""
use Ash.Resource.Change use Ash.Resource.Change
def change(changeset, opts, _) do def change(changeset, opts, _) do

View file

@ -1,4 +1,8 @@
defmodule AshHq.Docs.Extensions.RenderMarkdown do defmodule AshHq.Docs.Extensions.RenderMarkdown do
@moduledoc """
Sets up markdown text attributes to be transformed to html (in another column).
"""
@render_markdown %Ash.Dsl.Section{ @render_markdown %Ash.Dsl.Section{
name: :render_markdown, name: :render_markdown,
schema: [ schema: [

View file

@ -1,4 +1,11 @@
defmodule AshHq.Docs.Extensions.RenderMarkdown.Transformers.AddRenderMarkdownStructure do defmodule AshHq.Docs.Extensions.RenderMarkdown.Transformers.AddRenderMarkdownStructure do
@moduledoc """
Adds the resource structure required for the render markdown extension
Currently, this simply adds the relevant change and adds the destination
attributes to the `allow_nil_input` of each action, since it will be adding them automatically.
"""
use Ash.Dsl.Transformer use Ash.Dsl.Transformer
alias Ash.Dsl.Transformer alias Ash.Dsl.Transformer
@ -34,7 +41,5 @@ defmodule AshHq.Docs.Extensions.RenderMarkdown.Transformers.AddRenderMarkdownStr
end) end)
end end
def after?(Ash.Resource.Transformers.DefaultAccept), do: true def after?(_), do: true
def after?(Ash.Resource.Transformers.SetPrimaryActions), do: true
def after?(_), do: false
end end

View file

@ -1,4 +1,8 @@
defmodule AshHq.Docs.Extensions.Search.Changes.SanitizeName do defmodule AshHq.Docs.Extensions.Search.Changes.SanitizeName do
@moduledoc """
Writes the sanitized (url-safe) name of a record
"""
use Ash.Resource.Change use Ash.Resource.Change
def change(changeset, opts, _) do def change(changeset, opts, _) do

View file

@ -1,4 +1,7 @@
defmodule AshHq.Extensions.Search.Preparations.LoadSearchData do defmodule AshHq.Extensions.Search.Preparations.LoadSearchData do
@moduledoc """
Ensures that any data needed for search results is loaded.
"""
use Ash.Resource.Preparation use Ash.Resource.Preparation
def prepare(query, _, _) do def prepare(query, _, _) do
@ -10,7 +13,7 @@ defmodule AshHq.Extensions.Search.Preparations.LoadSearchData do
|> Ash.Query.load( |> Ash.Query.load(
search_headline: [query: query_string], search_headline: [query: query_string],
match_rank: [query: query_string], match_rank: [query: query_string],
name_matches: %{query: query_string, similarity: 0.7} name_matches: [query: query_string, similarity: 0.7]
) )
|> Ash.Query.load(to_load) |> Ash.Query.load(to_load)
|> Ash.Query.sort(match_rank: {:asc, %{query: query_string}}) |> Ash.Query.sort(match_rank: {:asc, %{query: query_string}})

View file

@ -1,4 +1,10 @@
defmodule AshHq.Docs.Extensions.Search do defmodule AshHq.Docs.Extensions.Search do
@moduledoc """
Sets a resource up to be searchable. See the configuration for explanation of the options.
This generally involves ensuring that there is a url safe name attribute to be used in routing,
and configuring how the item will be searched for.
"""
alias Ash.Dsl.Extension alias Ash.Dsl.Extension
@search %Ash.Dsl.Section{ @search %Ash.Dsl.Section{
@ -24,6 +30,12 @@ defmodule AshHq.Docs.Extensions.Search do
doc: doc:
"The name of the attribute to store the sanitized name in. If not set, will default to the `sanitized_<name_attribute>`" "The name of the attribute to store the sanitized name in. If not set, will default to the `sanitized_<name_attribute>`"
], ],
auto_sanitize_name_attribute?: [
type: :boolean,
default: true,
doc:
"Wether or not the name attribute will be sanitized by default. If not, you should have a change on the resource that sets it."
],
show_docs_on: [ show_docs_on: [
type: :atom, type: :atom,
doc: doc:
@ -72,6 +84,7 @@ defmodule AshHq.Docs.Extensions.Search do
Extension.get_opt(resource, [:search], :doc_attribute, nil) Extension.get_opt(resource, [:search], :doc_attribute, nil)
end end
# sobelow_skip ["DOS.BinToAtom"]
def sanitized_name_attribute(resource) do def sanitized_name_attribute(resource) do
Extension.get_opt( Extension.get_opt(
resource, resource,
@ -81,6 +94,15 @@ defmodule AshHq.Docs.Extensions.Search do
) )
end end
def auto_sanitize_name_attribute?(resource) do
Extension.get_opt(
resource,
[:search],
:auto_sanitize_name_attribute?,
true
)
end
def use_path_for_name?(resource) do def use_path_for_name?(resource) do
Extension.get_opt( Extension.get_opt(
resource, resource,

View file

@ -1,4 +1,18 @@
defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
@moduledoc """
Adds the resource structure required by the search extension.
* Adds a sanitized name attribute if it doesn't already exist
* Adds a change to set the sanitized name, if it should.
* Adds a `search_headline` calculation
* Adds a `name_matches` calculation
* Adds a `matches` calculation
* Adds relevant indexes using custom sql statements
* Adds an `html_for` calculation, that shows the html if a certain field matches, so docs are only shown on the right pages
* Adds a `match_rank` calculation.
* Adds a search action
* Adds a code interface for the search action
"""
use Ash.Dsl.Transformer use Ash.Dsl.Transformer
import Ash.Filter.TemplateHelpers import Ash.Filter.TemplateHelpers
require Ash.Query require Ash.Query
@ -19,9 +33,9 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
{:ok, {:ok,
dsl_state dsl_state
|> add_code_interface()
|> add_sanitized_name(config) |> add_sanitized_name(config)
|> add_search_action(config) |> add_search_action(config)
|> add_code_interface()
|> add_search_headline_calculation(config) |> add_search_headline_calculation(config)
|> add_name_matches_calculation(config) |> add_name_matches_calculation(config)
|> add_matches_calculation(config) |> add_matches_calculation(config)
@ -55,28 +69,39 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
end end
defp add_sanitized_name(dsl_state, config) do defp add_sanitized_name(dsl_state, config) do
dsl_state dsl_state =
|> Transformer.add_entity( if Ash.Resource.Info.attribute(config.resource, config.sanitized_name_attribute) do
[:attributes], dsl_state
Transformer.build_entity!( else
Ash.Resource.Dsl, Transformer.add_entity(
[:attributes], dsl_state,
:attribute, [:attributes],
name: config.sanitized_name_attribute, Transformer.build_entity!(
type: :string, Ash.Resource.Dsl,
allow_nil?: false [:attributes],
:attribute,
name: config.sanitized_name_attribute,
type: :string,
allow_nil?: false
)
)
end
if AshHq.Docs.Extensions.Search.auto_sanitize_name_attribute?(config.resource) do
Transformer.add_entity(
dsl_state,
[:changes],
Transformer.build_entity!(Ash.Resource.Dsl, [:changes], :change,
change:
{AshHq.Docs.Extensions.Search.Changes.SanitizeName,
source: config.name_attribute,
destination: config.sanitized_name_attribute,
use_path_for_name?: AshHq.Docs.Extensions.Search.use_path_for_name?(config.resource)}
)
) )
) else
|> Transformer.add_entity( dsl_state
[:changes], end
Transformer.build_entity!(Ash.Resource.Dsl, [:changes], :change,
change:
{AshHq.Docs.Extensions.Search.Changes.SanitizeName,
source: config.name_attribute,
destination: config.sanitized_name_attribute,
use_path_for_name?: AshHq.Docs.Extensions.Search.use_path_for_name?(config.resource)}
)
)
end end
defp add_indexes(dsl_state, config) do defp add_indexes(dsl_state, config) do
@ -275,7 +300,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
calculation: calculation:
Ash.Query.expr( Ash.Query.expr(
fragment( fragment(
"ts_headline('english', ?, plainto_tsquery('english', ?), 'MaxFragments=2,StartSel=\"<span class=\"\"search-hit\"\">\", StopSel=</span>')", ~S[ts_headline('english', ?, plainto_tsquery('english', ?), 'MaxFragments=2,StartSel=\"<span class=\"\"search-hit\"\">\", StopSel=</span>')],
^ref(config.doc_attribute), ^ref(config.doc_attribute),
^arg(:query) ^arg(:query)
) )
@ -296,7 +321,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
end end
end end
defp html_for_argument() do defp html_for_argument do
Transformer.build_entity!( Transformer.build_entity!(
Ash.Resource.Dsl, Ash.Resource.Dsl,
[:calculations, :calculate], [:calculations, :calculate],
@ -307,7 +332,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
) )
end end
defp query_argument() do defp query_argument do
Transformer.build_entity!( Transformer.build_entity!(
Ash.Resource.Dsl, Ash.Resource.Dsl,
[:calculations, :calculate], [:calculations, :calculate],
@ -318,7 +343,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
) )
end end
defp similarity_argument() do defp similarity_argument do
Transformer.build_entity!( Transformer.build_entity!(
Ash.Resource.Dsl, Ash.Resource.Dsl,
[:calculations, :calculate], [:calculations, :calculate],
@ -357,7 +382,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
) )
end end
defp search_arguments() do defp search_arguments do
[ [
Transformer.build_entity!( Transformer.build_entity!(
Ash.Resource.Dsl, Ash.Resource.Dsl,
@ -376,7 +401,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
] ]
end end
defp search_preparations() do defp search_preparations do
[ [
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :read], :prepare, Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :read], :prepare,
preparation: AshHq.Extensions.Search.Preparations.LoadSearchData preparation: AshHq.Extensions.Search.Preparations.LoadSearchData

View file

@ -1,4 +1,8 @@
defmodule AshHq.Docs.Extensions.Search.Types do defmodule AshHq.Docs.Extensions.Search.Types do
@moduledoc """
A static list of all search types that currently exist
"""
@search_types AshHq.Docs.Registry @search_types AshHq.Docs.Registry
|> Ash.Registry.entries() |> Ash.Registry.entries()
|> Enum.filter( |> Enum.filter(
@ -7,7 +11,7 @@ defmodule AshHq.Docs.Extensions.Search.Types do
|> Enum.map(&AshHq.Docs.Extensions.Search.type/1) |> Enum.map(&AshHq.Docs.Extensions.Search.type/1)
|> Enum.uniq() |> Enum.uniq()
def types() do def types do
@search_types @search_types
end end
end end

View file

@ -1,89 +1,95 @@
defmodule AshHq.Docs.Search do defmodule AshHq.Docs.Search do
@moduledoc false
use Ash.Flow use Ash.Flow
flow do flow do
api(AshHq.Docs) api AshHq.Docs
description """
Runs a search over all searchable items.
"""
argument :query, :string do argument :query, :string do
allow_nil?(false) allow_nil? false
constraints trim?: false, allow_empty?: true constraints trim?: false, allow_empty?: true
end end
argument :library_versions, {:array, :uuid} do argument :library_versions, {:array, :uuid} do
allow_nil?(false) allow_nil? false
end end
argument(:types, {:array, :string}) argument :types, {:array, :string}
returns(:build_results) returns :build_results
end end
steps do steps do
custom :options, AshHq.Docs.Search.Steps.SearchResource do custom :options, AshHq.Docs.Search.Steps.SearchResource do
input(%{ input %{
query: arg(:query), query: arg(:query),
library_versions: arg(:library_versions), library_versions: arg(:library_versions),
types: arg(:types), types: arg(:types),
resource: AshHq.Docs.Option resource: AshHq.Docs.Option
}) }
end end
custom :dsls, AshHq.Docs.Search.Steps.SearchResource do custom :dsls, AshHq.Docs.Search.Steps.SearchResource do
input(%{ input %{
query: arg(:query), query: arg(:query),
library_versions: arg(:library_versions), library_versions: arg(:library_versions),
types: arg(:types), types: arg(:types),
resource: AshHq.Docs.Dsl resource: AshHq.Docs.Dsl
}) }
end end
custom :guides, AshHq.Docs.Search.Steps.SearchResource do custom :guides, AshHq.Docs.Search.Steps.SearchResource do
input(%{ input %{
query: arg(:query), query: arg(:query),
library_versions: arg(:library_versions), library_versions: arg(:library_versions),
types: arg(:types), types: arg(:types),
resource: AshHq.Docs.Guide resource: AshHq.Docs.Guide
}) }
end end
custom :library_versions, AshHq.Docs.Search.Steps.SearchResource do custom :library_versions, AshHq.Docs.Search.Steps.SearchResource do
input(%{ input %{
query: arg(:query), query: arg(:query),
library_versions: arg(:library_versions), library_versions: arg(:library_versions),
types: arg(:types), types: arg(:types),
resource: AshHq.Docs.LibraryVersion resource: AshHq.Docs.LibraryVersion
}) }
end end
custom :extensions, AshHq.Docs.Search.Steps.SearchResource do custom :extensions, AshHq.Docs.Search.Steps.SearchResource do
input(%{ input %{
query: arg(:query), query: arg(:query),
library_versions: arg(:library_versions), library_versions: arg(:library_versions),
types: arg(:types), types: arg(:types),
resource: AshHq.Docs.Extension resource: AshHq.Docs.Extension
}) }
end end
custom :functions, AshHq.Docs.Search.Steps.SearchResource do custom :functions, AshHq.Docs.Search.Steps.SearchResource do
input(%{ input %{
query: arg(:query), query: arg(:query),
library_versions: arg(:library_versions), library_versions: arg(:library_versions),
types: arg(:types), types: arg(:types),
resource: AshHq.Docs.Function resource: AshHq.Docs.Function
}) }
end end
custom :modules, AshHq.Docs.Search.Steps.SearchResource do custom :modules, AshHq.Docs.Search.Steps.SearchResource do
input(%{ input %{
query: arg(:query), query: arg(:query),
library_versions: arg(:library_versions), library_versions: arg(:library_versions),
types: arg(:types), types: arg(:types),
resource: AshHq.Docs.Module resource: AshHq.Docs.Module
}) }
end end
custom :build_results, AshHq.Docs.Search.Steps.BuildResults do custom :build_results, AshHq.Docs.Search.Steps.BuildResults do
input(%{ input %{
dsls: result(:dsls), dsls: result(:dsls),
options: result(:options), options: result(:options),
guides: result(:guides), guides: result(:guides),
@ -91,7 +97,7 @@ defmodule AshHq.Docs.Search do
extensions: result(:extensions), extensions: result(:extensions),
functions: result(:functions), functions: result(:functions),
modules: result(:modules) modules: result(:modules)
}) }
end end
end end
end end

View file

@ -1,4 +1,7 @@
defmodule AshHq.Docs.Search.Steps.BuildResults do defmodule AshHq.Docs.Search.Steps.BuildResults do
@moduledoc """
Sorts the results of search.
"""
use Ash.Flow.Step use Ash.Flow.Step
def run(input, _opts, _context) do def run(input, _opts, _context) do

View file

@ -1,127 +0,0 @@
defmodule AshHq.Docs.Search.Steps.RunSearch do
# use Ash.Flow.Step
# import Ecto.Query, only: [from: 2]
# require Ecto.Query
# require Ash.Query
# @resources AshHq.Docs.Registry
# |> Ash.Registry.entries()
# |> Enum.filter(&(AshHq.Docs.Extensions.Search in Ash.Resource.Info.extensions(&1)))
# def run(input, _opts, _context) do
# @resources
# |> Enum.reduce(nil, fn resource, query ->
# {:ok, next_query} =
# resource
# |> Ash.Query.for_read(:search, %{
# library_versions: input[:library_versions],
# query: input[:query]
# })
# |> Ash.Query.select([:id])
# |> Ash.Query.data_layer_query()
# next_query =
# from row in next_query,
# select_merge: %{__metadata__: %{resource: ^to_string(resource)}}
# if query do
# Ecto.Query.union_all(query, ^next_query)
# else
# next_query
# end
# end)
# |> then(fn query ->
# query =
# from row in query,
# order_by: [
# fragment(
# "ts_rank(setweight(to_tsvector(?), 'A') || setweight(to_tsvector(?), 'D'), plainto_tsquery(?))",
# field(row, ^name_attribute),
# field(row, ^doc_attribute),
# ^input[:query]
# )
# ],
# limit: 20
# ids = AshHq.Repo.all(query)
# data =
# ids
# |> Enum.group_by(& &1.__metadata__.resource, & &1)
# |> Enum.reduce(%{}, fn {resource, id_data}, results ->
# resource = Module.concat([resource])
# primary_read = Ash.Resource.Info.primary_action!(resource, :read).name
# to_load = AshHq.Docs.Extensions.Search.load_for_search(resource)
# ids = Enum.map(id_data, & &1.id)
# resource
# |> Ash.Query.filter(id in ^ids)
# |> Ash.Query.load(
# search_headline: %{query: input[:query]},
# name_matches: %{query: input[:query], similarity: 0.7},
# match_rank: %{query: input[:query]}
# )
# |> Ash.Query.load(to_load)
# |> Ash.Query.for_read(primary_read)
# |> AshHq.Docs.read!()
# |> Enum.reduce(results, fn item, results ->
# Map.put(results, item.id, item)
# end)
# end)
# {:ok,
# Enum.map(ids, fn %{id: id} ->
# Map.fetch!(data, id)
# end)}
# end)
# end
# # read :options, AshHq.Docs.Option, :search do
# # input %{
# # library_versions: arg(:library_versions),
# # query: arg(:query)
# # }
# # end
# # read :dsls, AshHq.Docs.Dsl, :search do
# # input %{
# # library_versions: arg(:library_versions),
# # query: arg(:query)
# # }
# # end
# # read :guides, AshHq.Docs.Guide, :search do
# # input %{
# # library_versions: arg(:library_versions),
# # query: arg(:query)
# # }
# # end
# # read :library_versions, AshHq.Docs.LibraryVersion, :search do
# # input %{
# # library_versions: arg(:library_versions),
# # query: arg(:query)
# # }
# # end
# # read :extensions, AshHq.Docs.Extension, :search do
# # input %{
# # library_versions: arg(:library_versions),
# # query: arg(:query)
# # }
# # end
# # read :functions, AshHq.Docs.Function, :search do
# # input %{
# # library_versions: arg(:library_versions),
# # query: arg(:query)
# # }
# # end
# # read :modules, AshHq.Docs.Module, :search do
# # input %{
# # library_versions: arg(:library_versions),
# # query: arg(:query)
# # }
# # end
end

View file

@ -1,4 +1,7 @@
defmodule AshHq.Docs.Search.Steps.SearchResource do defmodule AshHq.Docs.Search.Steps.SearchResource do
@moduledoc """
Runs the search action of a given resource, or skips it if it should not be included in the results.
"""
use Ash.Flow.Step use Ash.Flow.Step
def run(input, _opts, _context) do def run(input, _opts, _context) do

View file

@ -7,6 +7,7 @@ defmodule AshHq.Docs.Importer do
require Logger require Logger
require Ash.Query require Ash.Query
# sobelow_skip ["Misc.BinToTerm", "Traversal.FileModule"]
def import(opts \\ []) do def import(opts \\ []) do
only = opts[:only] || nil only = opts[:only] || nil
only_branches? = opts[:only_branches?] || false only_branches? = opts[:only_branches?] || false

View file

@ -1,4 +1,5 @@
defmodule AshHq.Docs.Registry do defmodule AshHq.Docs.Registry do
@moduledoc false
use Ash.Registry, use Ash.Registry,
extensions: [Ash.Registry.ResourceValidations] extensions: [Ash.Registry.ResourceValidations]

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.Dsl do defmodule AshHq.Docs.Dsl do
@moduledoc false
use AshHq.Resource, use AshHq.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown] extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
resource do
description "An entity or section in an Ash DSL"
end
render_markdown do render_markdown do
render_attributes doc: :doc_html render_attributes doc: :doc_html
end end

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.Extension do defmodule AshHq.Docs.Extension do
@moduledoc false
use AshHq.Resource, use AshHq.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown] extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
resource do
description "An Ash DSL extension."
end
render_markdown do render_markdown do
render_attributes doc: :doc_html render_attributes doc: :doc_html
end end

View file

@ -1,22 +1,28 @@
defmodule AshHq.Docs.Function do defmodule AshHq.Docs.Function do
@moduledoc false
use AshHq.Resource, use AshHq.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown] extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
resource do
description "A function in a module exposed by an Ash library"
end
render_markdown do render_markdown do
render_attributes(doc: :doc_html) render_attributes doc: :doc_html
header_ids?(false) header_ids? false
end end
search do search do
doc_attribute :doc doc_attribute :doc
load_for_search([ load_for_search [
:version_name, :version_name,
:library_name, :library_name,
:module_name, :module_name,
:library_id :library_id
]) ]
type "Code" type "Code"
@ -28,7 +34,7 @@ defmodule AshHq.Docs.Function do
repo AshHq.Repo repo AshHq.Repo
references do references do
reference(:library_version, on_delete: :delete) reference :library_version, on_delete: :delete
end end
end end

View file

@ -1,4 +1,7 @@
defmodule AshHq.Docs.Guide.Changes.SetRoute do defmodule AshHq.Docs.Guide.Changes.SetRoute do
@moduledoc """
Sets the route of a guide.
"""
use Ash.Resource.Change use Ash.Resource.Change
def change(changeset, _, _) do def change(changeset, _, _) do

View file

@ -1,8 +1,13 @@
defmodule AshHq.Docs.Guide do defmodule AshHq.Docs.Guide do
@moduledoc false
use AshHq.Resource, use AshHq.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown] extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
resource do
description "Represents a markdown guide exposed by a library"
end
render_markdown do render_markdown do
render_attributes text: :text_html render_attributes text: :text_html
end end
@ -12,6 +17,8 @@ defmodule AshHq.Docs.Guide do
type "Guides" type "Guides"
load_for_search library_version: [:library_name, :library_display_name] load_for_search library_version: [:library_name, :library_display_name]
show_docs_on :route show_docs_on :route
sanitized_name_attribute :route
auto_sanitize_name_attribute?(false)
end end
code_interface do code_interface do
@ -19,12 +26,7 @@ defmodule AshHq.Docs.Guide do
end end
actions do actions do
defaults [:read, :update, :destroy] defaults [:create, :read, :update, :destroy]
create :create do
primary? true
allow_nil_input [:route]
end
end end
changes do changes do

View file

@ -1,7 +1,12 @@
defmodule AshHq.Docs.Library do defmodule AshHq.Docs.Library do
@moduledoc false
use AshHq.Resource, use AshHq.Resource,
data_layer: AshPostgres.DataLayer data_layer: AshPostgres.DataLayer
resource do
description "Represents a library that will be imported into AshHq"
end
postgres do postgres do
table "libraries" table "libraries"
repo AshHq.Repo repo AshHq.Repo

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.LibraryVersion do defmodule AshHq.Docs.LibraryVersion do
@moduledoc false
use AshHq.Resource, use AshHq.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown] extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
resource do
description "Represents a version of a library that has been imported."
end
search do search do
name_attribute :version name_attribute :version
library_version_attribute :id library_version_attribute :id

View file

@ -1,4 +1,7 @@
defmodule AshHq.Docs.LibraryVersion.Preparations.SortBySortableVersionInstead do defmodule AshHq.Docs.LibraryVersion.Preparations.SortBySortableVersionInstead do
@moduledoc """
Replaces any sort on `version` by a sort on `sortable_version` instead.
"""
use Ash.Resource.Preparation use Ash.Resource.Preparation
def prepare(query, _, _) do def prepare(query, _, _) do
@ -6,8 +9,8 @@ defmodule AshHq.Docs.LibraryVersion.Preparations.SortBySortableVersionInstead do
end end
defp replace_sort(nil), do: nil defp replace_sort(nil), do: nil
defp replace_sort(:version), do: :version defp replace_sort(:version), do: :sortable_version
defp replace_sort({:version, order}), do: {:version, order} defp replace_sort({:version, order}), do: {:sortable_version, order}
defp replace_sort(list) when is_list(list), do: Enum.map(list, &replace_sort/1) defp replace_sort(list) when is_list(list), do: Enum.map(list, &replace_sort/1)
defp replace_sort(other), do: other defp replace_sort(other), do: other
end end

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.Module do defmodule AshHq.Docs.Module do
@moduledoc false
use AshHq.Resource, use AshHq.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown] extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
resource do
description "Represents a module that has been exposed by a library"
end
render_markdown do render_markdown do
render_attributes doc: :doc_html render_attributes doc: :doc_html
end end

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.Option do defmodule AshHq.Docs.Option do
@moduledoc false
use AshHq.Resource, use AshHq.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown] extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
resource do
description "Represents an option on a DSL section or entity"
end
render_markdown do render_markdown do
render_attributes doc: :doc_html render_attributes doc: :doc_html
end end

View file

@ -1,17 +0,0 @@
defmodule AshHq.Guardian do
use Guardian, otp_app: :ash_hq
alias AshHq.Accounts
def subject_for_token(resource, _claims) do
sub = to_string(resource.id)
{:ok, sub}
end
def resource_from_claims(claims) do
id = claims["sub"]
resource = Accounts.get!(Accounts.User, id)
{:ok, resource}
end
end

View file

@ -2,7 +2,7 @@ defmodule AshHq.Repo do
use AshPostgres.Repo, use AshPostgres.Repo,
otp_app: :ash_hq otp_app: :ash_hq
def installed_extensions() do def installed_extensions do
["pg_trgm", "uuid-ossp", "citext"] ["pg_trgm", "uuid-ossp", "citext"]
end end
end end

View file

@ -1,31 +1,8 @@
defmodule AshHq.Resource do defmodule AshHq.Resource do
@moduledoc "AshHq's base resource."
defmacro __using__(opts) do defmacro __using__(opts) do
opts =
if opts[:notifiers] && Ash.Notifier.PubSub in opts[:notifiers] do
opts
else
opts
|> Keyword.put_new(:notifiers, [])
|> Keyword.update!(:notifiers, &[Ash.Notifier.PubSub | &1])
end
quote do quote do
use Ash.Resource, unquote(opts) use Ash.Resource, unquote(opts)
pub_sub do
module AshHqWeb.Endpoint
prefix Module.split(__MODULE__)
|> Enum.reverse()
|> Enum.take(2)
|> Enum.reverse()
|> Enum.map(&Macro.underscore/1)
|> Enum.join(".")
publish_all :create, ["created"]
publish_all :update, ["updated"]
publish_all :destroy, ["destroyed"]
end
end end
end end
end end

View file

@ -1,4 +1,5 @@
defmodule AshHqWeb.Components.CalloutText do defmodule AshHqWeb.Components.CalloutText do
@moduledoc "Highlights some text on the page"
use Surface.Component use Surface.Component
slot default, required: true slot default, required: true

View file

@ -1,14 +1,14 @@
defmodule AshHqWeb.Components.CodeExample do defmodule AshHqWeb.Components.CodeExample do
@moduledoc "Renders a code example, as seen on the home page"
use Surface.LiveComponent use Surface.LiveComponent
prop text, :string, required: true prop code, :string, required: true
prop class, :css_class prop class, :css_class
prop title, :string prop title, :string
prop start_collapsed, :boolean, default: false prop start_collapsed, :boolean, default: false
prop collapsible, :boolean, default: false prop collapsible, :boolean, default: false
data collapsed, :string, default: false data collapsed, :string, default: false
data code, :string, default: ""
def render(assigns) do def render(assigns) do
~F""" ~F"""
@ -73,19 +73,17 @@ defmodule AshHqWeb.Components.CodeExample do
{:ok, {:ok,
socket socket
|> assign(assigns) |> assign(assigns)
|> assign(:collapsed, true) |> assign(:collapsed, true)}
|> assign(:code, to_code(assigns[:text]))}
else else
{:ok, {:ok,
socket socket
|> assign(assigns) |> assign(assigns)
|> assign(:collapsed, false) |> assign(:collapsed, false)}
|> assign(:code, to_code(assigns[:text]))}
end end
end end
defp to_code(text) do @doc false
# TODO: do this at compile time def to_code(text) do
lines = lines =
text text
# this is pretty naive, won't handle things like block comments # this is pretty naive, won't handle things like block comments

View file

@ -1,7 +1,8 @@
defmodule AshHqWeb.Components.DocSidebar do defmodule AshHqWeb.Components.DocSidebar do
@moduledoc "The left sidebar of the docs pages"
use Surface.Component use Surface.Component
alias AshHqWeb.Routes alias AshHqWeb.DocRoutes
alias Surface.Components.LiveRedirect alias Surface.Components.LiveRedirect
prop class, :css_class, default: "" prop class, :css_class, default: ""
@ -29,21 +30,21 @@ defmodule AshHqWeb.Components.DocSidebar do
</div> </div>
{#for {category, guides} <- guides_by_category(@libraries)} {#for {category, guides} <- guides_by_category(@libraries)}
<div class="text-gray-500"> <div class="text-gray-500">
{#if @sidebar_state["guides-#{Routes.sanitize_name(category)}"] == "open" || (@guide && Enum.any?(guides, &(&1.id == @guide.id))) || (@sidebar_state["guides-#{Routes.sanitize_name(category)}"] != "closed" && category == "Tutorials")} {#if @sidebar_state["guides-#{DocRoutes.sanitize_name(category)}"] == "open" || (@guide && Enum.any?(guides, &(&1.id == @guide.id))) || (@sidebar_state["guides-#{DocRoutes.sanitize_name(category)}"] != "closed" && category == "Tutorials")}
<button :on-click={@collapse_sidebar} phx-value-id={"guides-#{Routes.sanitize_name(category)}"} class="flex flex-row items-center"> <button :on-click={@collapse_sidebar} phx-value-id={"guides-#{DocRoutes.sanitize_name(category)}"} class="flex flex-row items-center">
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" /><div>{category}</div> <Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" /><div>{category}</div>
</button> </button>
{#else} {#else}
<button :on-click={@expand_sidebar} phx-value-id={"guides-#{Routes.sanitize_name(category)}"} class="flex flex-row items-center"> <button :on-click={@expand_sidebar} phx-value-id={"guides-#{DocRoutes.sanitize_name(category)}"} class="flex flex-row items-center">
<Heroicons.Outline.ChevronRightIcon class="w-3 h-3 mr-1" /><div>{category}</div> <Heroicons.Outline.ChevronRightIcon class="w-3 h-3 mr-1" /><div>{category}</div>
</button> </button>
{/if} {/if}
</div> </div>
{#if @sidebar_state["guides-#{Routes.sanitize_name(category)}"] == "open" || (@guide && Enum.any?(guides, &(&1.id == @guide.id))) || (@sidebar_state["guides-#{Routes.sanitize_name(category)}"] != "closed" && category == "Tutorials")} {#if @sidebar_state["guides-#{DocRoutes.sanitize_name(category)}"] == "open" || (@guide && Enum.any?(guides, &(&1.id == @guide.id))) || (@sidebar_state["guides-#{DocRoutes.sanitize_name(category)}"] != "closed" && category == "Tutorials")}
{#for guide <- guides} {#for guide <- guides}
<li class="ml-3"> <li class="ml-3">
<LiveRedirect <LiveRedirect
to={Routes.doc_link(guide, @selected_versions)} to={DocRoutes.doc_link(guide, @selected_versions)}
class={ class={
"flex items-center p-1 text-base font-normal text-gray-900 rounded-lg dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700", "flex items-center p-1 text-base font-normal text-gray-900 rounded-lg dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700",
"bg-gray-300 dark:bg-gray-600": @guide && @guide.id == guide.id "bg-gray-300 dark:bg-gray-600": @guide && @guide.id == guide.id
@ -75,7 +76,7 @@ defmodule AshHqWeb.Components.DocSidebar do
{#for extension <- get_extensions(@libraries)} {#for extension <- get_extensions(@libraries)}
<li class="ml-3"> <li class="ml-3">
<LiveRedirect <LiveRedirect
to={Routes.doc_link(extension, @selected_versions)} to={DocRoutes.doc_link(extension, @selected_versions)}
class={ class={
"flex items-center p-1 text-base font-normal text-gray-900 rounded-lg dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700", "flex items-center p-1 text-base font-normal text-gray-900 rounded-lg dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700",
"dark:bg-gray-600": @extension && @extension.id == extension.id "dark:bg-gray-600": @extension && @extension.id == extension.id
@ -111,7 +112,7 @@ defmodule AshHqWeb.Components.DocSidebar do
{#for module <- modules} {#for module <- modules}
<li class="ml-4"> <li class="ml-4">
<LiveRedirect <LiveRedirect
to={Routes.doc_link(module, @selected_versions)} to={DocRoutes.doc_link(module, @selected_versions)}
class={ class={
"flex items-center pt-1 text-base font-normal text-gray-900 rounded-lg dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700", "flex items-center pt-1 text-base font-normal text-gray-900 rounded-lg dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700",
"dark:bg-gray-600": @module && @module.id == module.id "dark:bg-gray-600": @module && @module.id == module.id
@ -173,7 +174,7 @@ defmodule AshHqWeb.Components.DocSidebar do
{/if} {/if}
{/if} {/if}
<LiveRedirect <LiveRedirect
to={Routes.doc_link(dsl, @selected_versions)} to={DocRoutes.doc_link(dsl, @selected_versions)}
class={ class={
"flex items-center p-1 text-base font-normal rounded-lg hover:text-orange-300", "flex items-center p-1 text-base font-normal rounded-lg hover:text-orange-300",
"text-orange-600 dark:text-orange-400 font-bold": @dsl && @dsl.id == dsl.id "text-orange-600 dark:text-orange-400 font-bold": @dsl && @dsl.id == dsl.id

View file

@ -1,49 +0,0 @@
defmodule AshHqWeb.Components.ProgressiveHeading do
use Surface.Component
prop depth, :integer, required: true
slot default, required: true
def render(assigns) do
~F"""
{#case @depth}
{#match 1}
<h1>
<#slot />
</h1>
{#match 2}
<h2>
<#slot />
</h2>
{#match 3}
<h3>
<#slot />
</h3>
{#match 4}
<h4>
<#slot />
</h4>
{#match 5}
<h5>
<#slot />
</h5>
{#match 6}
<h6>
<#slot />
</h6>
{#match 7}
<span>
<#slot />
</span>
{#match 8}
<span>
<#slot />
</span>
{#match 9}
<span>
<#slot />
</span>
{/case}
"""
end
end

View file

@ -1,8 +1,9 @@
defmodule AshHqWeb.Components.RightNav do defmodule AshHqWeb.Components.RightNav do
@moduledoc "The right nav shown for functions in a module."
use Surface.Component use Surface.Component
prop(functions, :list, default: []) prop functions, :list, default: []
prop(module, :string, required: true) prop module, :string, required: true
def render(assigns) do def render(assigns) do
~F""" ~F"""

View file

@ -1,10 +1,11 @@
defmodule AshHqWeb.Components.Search do defmodule AshHqWeb.Components.Search do
@moduledoc "The search overlay modal"
use Surface.LiveComponent use Surface.LiveComponent
require Ash.Query require Ash.Query
alias AshHqWeb.Routes
alias AshHqWeb.Components.CalloutText alias AshHqWeb.Components.CalloutText
alias AshHqWeb.DocRoutes
alias Surface.Components.{Form, LiveRedirect} alias Surface.Components.{Form, LiveRedirect}
alias Surface.Components.Form.{Checkbox, Label, Select} alias Surface.Components.Form.{Checkbox, Label, Select}
@ -108,7 +109,7 @@ defmodule AshHqWeb.Components.Search do
defp render_items(assigns, items) do defp render_items(assigns, items) do
~F""" ~F"""
{#for item <- items} {#for item <- items}
<LiveRedirect to={Routes.doc_link(item, @selected_versions)} opts={id: item.id}> <LiveRedirect to={DocRoutes.doc_link(item, @selected_versions)} opts={id: item.id}>
<div class={ <div class={
"rounded-lg mb-4 py-2 px-2 hover:bg-gray-300 dark:hover:bg-gray-700", "rounded-lg mb-4 py-2 px-2 hover:bg-gray-300 dark:hover:bg-gray-700",
"bg-gray-400 dark:bg-gray-600": @selected_item.id == item.id, "bg-gray-400 dark:bg-gray-600": @selected_item.id == item.id,
@ -221,7 +222,7 @@ defmodule AshHqWeb.Components.Search do
{:noreply, socket} {:noreply, socket}
item -> item ->
{:noreply, push_redirect(socket, to: Routes.doc_link(item))} {:noreply, push_redirect(socket, to: DocRoutes.doc_link(item))}
end end
end end

View file

@ -1,4 +1,5 @@
defmodule AshHqWeb.Components.SearchBar do defmodule AshHqWeb.Components.SearchBar do
@moduledoc "A clickable search bar that brings up the search overlay"
use Surface.Component use Surface.Component
prop class, :css_class, default: "" prop class, :css_class, default: ""

View file

@ -1,8 +1,9 @@
defmodule AshHqWeb.Components.Tag do defmodule AshHqWeb.Components.Tag do
@moduledoc "Renders a simple pill style tag"
use Surface.Component use Surface.Component
prop(color, :atom, values: [:red]) prop color, :atom, values: [:red]
slot(default) slot default
def render(assigns) do def render(assigns) do
~F""" ~F"""

View file

@ -1,4 +1,5 @@
defmodule AshHqWeb.Routes do defmodule AshHqWeb.DocRoutes do
@moduledoc "Helpers for routing to results of searches"
def library_link(library, name) do def library_link(library, name) do
"/docs/dsl/#{library.name}/#{name}" "/docs/dsl/#{library.name}/#{name}"
end end

View file

@ -22,6 +22,9 @@ defmodule AshHqWeb.Endpoint do
gzip: false, gzip: false,
only: ~w(assets fonts images favicon.ico robots.txt) only: ~w(assets fonts images favicon.ico robots.txt)
# Pass configuration explicitly
plug PlugContentSecurityPolicy
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint. # :code_reloader configuration of your endpoint.
if code_reloading? do if code_reloading? do

View file

@ -1,4 +1,6 @@
defmodule AshHqWeb.Helpers do defmodule AshHqWeb.Helpers do
@moduledoc "Simple helpers for doc liveviews"
def latest_version(library) do def latest_version(library) do
Enum.find(library.versions, fn version -> Enum.find(library.versions, fn version ->
!String.contains?(version.version, ".") !String.contains?(version.version, ".")

View file

@ -0,0 +1,17 @@
defmodule AshHqWeb.LiveUserAuth do
@moduledoc """
Helpers for authenticating users in liveviews
"""
@doc """
Sets the current user on each mount of a liveview
"""
def on_mount(:live_user, _params, session, socket) do
{:cont,
Phoenix.LiveView.assign(
socket,
:current_user,
AshHqWeb.UserAuth.user_for_session_token(session["user_token"])
)}
end
end

View file

@ -1,30 +1,31 @@
defmodule AshHqWeb.Pages.Docs do defmodule AshHqWeb.Pages.Docs do
@moduledoc "The page for showing documentation"
use Surface.Component use Surface.Component
alias Phoenix.LiveView.JS
alias AshHq.Docs.Extensions.RenderMarkdown alias AshHq.Docs.Extensions.RenderMarkdown
alias AshHqWeb.Components.{CalloutText, DocSidebar, RightNav, Tag} alias AshHqWeb.Components.{CalloutText, DocSidebar, RightNav, Tag}
alias AshHqWeb.Routes alias AshHqWeb.DocRoutes
alias Phoenix.LiveView.JS
require Logger require Logger
prop(change_versions, :event, required: true) prop change_versions, :event, required: true
prop(selected_versions, :map, required: true) prop selected_versions, :map, required: true
prop(libraries, :list, default: []) prop libraries, :list, default: []
prop(uri, :string) prop uri, :string
prop(sidebar_state, :map, required: true) prop sidebar_state, :map, required: true
prop(collapse_sidebar, :event, required: true) prop collapse_sidebar, :event, required: true
prop(expand_sidebar, :event, required: true) prop expand_sidebar, :event, required: true
prop(library, :any) prop library, :any
prop(extension, :any) prop extension, :any
prop(docs, :any) prop docs, :any
prop(library_version, :any) prop library_version, :any
prop(guide, :any) prop guide, :any
prop(doc_path, :list, default: []) prop doc_path, :list, default: []
prop(dsls, :list, default: []) prop dsls, :list, default: []
prop(dsl, :any) prop dsl, :any
prop(options, :list, default: []) prop options, :list, default: []
prop(module, :any) prop module, :any
@spec render(any) :: Phoenix.LiveView.Rendered.t() @spec render(any) :: Phoenix.LiveView.Rendered.t()
def render(assigns) do def render(assigns) do
@ -137,7 +138,7 @@ defmodule AshHqWeb.Pages.Docs do
{#for mod <- imports} {#for mod <- imports}
<ul> <ul>
<li> <li>
<a href={Routes.doc_link(mod, @selected_versions)}>{mod.name}</a> <a href={DocRoutes.doc_link(mod, @selected_versions)}>{mod.name}</a>
</li> </li>
</ul> </ul>
{/for} {/for}
@ -151,7 +152,7 @@ defmodule AshHqWeb.Pages.Docs do
<ul> <ul>
{#for child <- children} {#for child <- children}
<li> <li>
<a href={Routes.doc_link(child, @selected_versions)}>{child.name}</a> <a href={DocRoutes.doc_link(child, @selected_versions)}>{child.name}</a>
</li> </li>
{/for} {/for}
</ul> </ul>
@ -281,13 +282,7 @@ defmodule AshHqWeb.Pages.Docs do
dsl.imports || [] dsl.imports || []
end) end)
|> Enum.flat_map(fn mod_name -> |> Enum.flat_map(fn mod_name ->
case Enum.find_value(libraries, fn library -> case find_module(libraries, selected_versions, mod_name) do
Enum.find_value(library.versions, fn version ->
if version.id == selected_versions[library.id] do
Enum.find(version.modules, &(&1.name == mod_name))
end
end)
end) do
nil -> nil ->
Logger.warn("No such module found called #{inspect(mod_name)}") Logger.warn("No such module found called #{inspect(mod_name)}")
[] []
@ -298,6 +293,16 @@ defmodule AshHqWeb.Pages.Docs do
end) end)
end end
defp find_module(libraries, selected_versions, mod_name) do
Enum.find_value(libraries, fn library ->
Enum.find_value(library.versions, fn version ->
if version.id == selected_versions[library.id] do
Enum.find(version.modules, &(&1.name == mod_name))
end
end)
end)
end
defp child_dsls(_, nil), do: [] defp child_dsls(_, nil), do: []
defp child_dsls(nil, _), do: [] defp child_dsls(nil, _), do: []
@ -363,7 +368,7 @@ defmodule AshHqWeb.Pages.Docs do
end end
def path_to_name(path, name) do def path_to_name(path, name) do
Enum.map_join(path ++ [name], "-", &Routes.sanitize_name/1) Enum.map_join(path ++ [name], "-", &DocRoutes.sanitize_name/1)
end end
defp render_tags(assigns, option) do defp render_tags(assigns, option) do
@ -376,8 +381,8 @@ defmodule AshHqWeb.Pages.Docs do
""" """
end end
def show_sidebar() do def show_sidebar(js \\ %JS{}) do
%JS{} js
|> JS.toggle( |> JS.toggle(
to: "#mobile-sidebar-container", to: "#mobile-sidebar-container",
in: { in: {
@ -493,7 +498,7 @@ defmodule AshHqWeb.Pages.Docs do
raise "No such guide in link: #{source}" raise "No such guide in link: #{source}"
""" """
<a href="#{Routes.doc_link(guide, assigns[:selected_versions])}">#{item}</a> <a href="#{DocRoutes.doc_link(guide, assigns[:selected_versions])}">#{item}</a>
""" """
"dsl" -> "dsl" ->
@ -508,12 +513,12 @@ defmodule AshHqWeb.Pages.Docs do
"module" -> "module" ->
""" """
<a href="/docs/module/#{library.name}/#{Routes.sanitize_name(version.version)}/#{Routes.sanitize_name(item)}">#{item}</a> <a href="/docs/module/#{library.name}/#{DocRoutes.sanitize_name(version.version)}/#{DocRoutes.sanitize_name(item)}">#{item}</a>
""" """
"extension" -> "extension" ->
""" """
<a href="/docs/dsl/#{library.name}/#{version.sanitized_version}/#{Routes.sanitize_name(item)}">#{item}</a> <a href="/docs/dsl/#{library.name}/#{version.sanitized_version}/#{DocRoutes.sanitize_name(item)}">#{item}</a>
""" """
type -> type ->

View file

@ -1,9 +1,10 @@
defmodule AshHqWeb.Pages.Home do defmodule AshHqWeb.Pages.Home do
@moduledoc "The home page"
use Surface.LiveComponent use Surface.LiveComponent
alias AshHqWeb.Components.{CalloutText, CodeExample, SearchBar} alias AshHqWeb.Components.{CalloutText, CodeExample, SearchBar}
import AshHqWeb.Components.CodeExample, only: [to_code: 1]
prop libraries, :list, default: []
def render(assigns) do def render(assigns) do
~F""" ~F"""
@ -26,7 +27,7 @@ defmodule AshHqWeb.Pages.Home do
<CodeExample <CodeExample
id="define-a-resource" id="define-a-resource"
class="grow min-w-fit max-w-[1000px]" class="grow min-w-fit max-w-[1000px]"
text={post_example()} code={post_example()}
title="Define a resource" title="Define a resource"
/> />
<div class="flex flex-col space-y-8"> <div class="flex flex-col space-y-8">
@ -34,14 +35,14 @@ defmodule AshHqWeb.Pages.Home do
class="w-auto" class="w-auto"
collapsible collapsible
id="use-it-programmatically" id="use-it-programmatically"
text={changeset_example()} code={changeset_example()}
title="Use it programmatically" title="Use it programmatically"
/> />
<CodeExample <CodeExample
class="w-auto" class="w-auto"
collapsible collapsible
id="graphql-interface" id="graphql-interface"
text={graphql_example()} code={graphql_example()}
title="Add a GraphQL interface" title="Add a GraphQL interface"
/> />
<CodeExample <CodeExample
@ -49,7 +50,7 @@ defmodule AshHqWeb.Pages.Home do
collapsible collapsible
start_collapsed start_collapsed
id="authorization-policies" id="authorization-policies"
text={policies_example()} code={policies_example()}
title="Add authorization policies" title="Add authorization policies"
/> />
<CodeExample <CodeExample
@ -57,7 +58,7 @@ defmodule AshHqWeb.Pages.Home do
collapsible collapsible
start_collapsed start_collapsed
id="aggregates" id="aggregates"
text={aggregate_example()} code={aggregate_example()}
title="Define aggregates and calculations" title="Define aggregates and calculations"
/> />
<CodeExample <CodeExample
@ -65,7 +66,7 @@ defmodule AshHqWeb.Pages.Home do
collapsible collapsible
start_collapsed start_collapsed
id="pubsub" id="pubsub"
text={notifier_example()} code={notifier_example()}
title="Broadcast changes over Phoenix PubSub" title="Broadcast changes over Phoenix PubSub"
/> />
<CodeExample <CodeExample
@ -73,7 +74,7 @@ defmodule AshHqWeb.Pages.Home do
collapsible collapsible
start_collapsed start_collapsed
id="live-view" id="live-view"
text={live_view_example()} code={live_view_example()}
title="Use it with Phoenix LiveView" title="Use it with Phoenix LiveView"
/> />
</div> </div>
@ -84,153 +85,170 @@ defmodule AshHqWeb.Pages.Home do
""" """
end end
defp changeset_example() do @changeset_example """
""" post = Example.Post.create!(%{
post = Example.Post.create!(%{ text: "Declarative programming is fun!"
text: "Declarative programming is fun!" })
})
Example.Post.react!(post, %{type: :like}) Example.Post.react!(post, %{type: :like})
Example.Post Example.Post
|> Ash.Query.filter(likes > 10) |> Ash.Query.filter(likes > 10)
|> Ash.Query.sort(likes: :desc) |> Ash.Query.sort(likes: :desc)
|> Example.read!() |> Example.read!()
""" """
|> to_code()
defp changeset_example do
@changeset_example
end end
defp live_view_example() do @live_view_example """
""" def mount(_params, _session, socket) do
def mount(_params, _session, socket) do form = AshPhoenix.Form.for_create(Example.Post, :create)
form = AshPhoenix.Form.for_create(Example.Post, :create)
{:ok, assign(socket, :form, form}} {:ok, assign(socket, :form, form}}
end end
def handle_event("validate", %{"form" => input}, socket) do def handle_event("validate", %{"form" => input}, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, input) form = AshPhoenix.Form.validate(socket.assigns.form, input)
{:ok, assign(socket, :form, form)} {:ok, assign(socket, :form, form)}
end end
def handle_event("submit", _, socket) do def handle_event("submit", _, socket) do
case AshPhoenix.Form.submit(socket.assigns.form) do case AshPhoenix.Form.submit(socket.assigns.form) do
{:ok, post} -> {:ok, post} ->
{:ok, redirect_to_post(socket, post)} {:ok, redirect_to_post(socket, post)}
{:error, form_with_errors} -> {:error, form_with_errors} ->
{:noreply, assign(socket, :form, form_with_errors)} {:noreply, assign(socket, :form, form_with_errors)}
end end
end end
""" """
|> to_code()
defp live_view_example do
@live_view_example
end end
defp graphql_example() do @graphql_example """
""" graphql do
graphql do type :post
type :post
queries do queries do
get :get_post, :read get :get_post, :read
list :feed, :read list :feed, :read
end end
mutations do mutations do
create :create_post, :create create :create_post, :create
update :react_to_post, :react update :react_to_post, :react
end end
end end
""" """
|> to_code()
defp graphql_example do
@graphql_example
end end
defp policies_example() do @policies_example """
""" policies do
policies do policy action_type(:read) do
policy action_type(:read) do authorize_if expr(visibility == :everyone)
authorize_if expr(visibility == :everyone) authorize_if relates_to_actor_via([:author, :friends])
authorize_if relates_to_actor_via([:author, :friends]) end
end end
end """
""" |> to_code()
defp policies_example do
@policies_example
end end
defp notifier_example() do @notifier_example """
""" pub_sub do
pub_sub do module ExampleEndpoint
module ExampleEndpoint prefix "post"
prefix "post"
publish_all :create, ["created"] publish_all :create, ["created"]
publish :react, ["reaction", :id] event: "reaction" publish :react, ["reaction", :id] event: "reaction"
end end
""" """
|> to_code()
defp notifier_example do
@notifier_example
end end
defp aggregate_example() do @aggregate_example """
""" aggregates do
aggregates do count :likes, :reactions do
count :likes, :reactions do filter expr(type == :like)
filter expr(type == :like) end
end
count :dislikes, :reactions do count :dislikes, :reactions do
filter expr(type == :dislike) filter expr(type == :dislike)
end end
end end
calculations do calculations do
calculate :like_ratio, :float do calculate :like_ratio, :float do
expr(likes / (likes + dislikes)) expr(likes / (likes + dislikes))
end end
end end
""" """
|> to_code()
defp aggregate_example do
@aggregate_example
end end
defp post_example() do @post_example """
""" defmodule Example.Post do
defmodule Example.Post do use AshHq.Resource,
use AshHq.Resource, data_layer: AshPostgres.DataLayer
data_layer: AshPostgres.DataLayer
postgres do postgres do
table "posts" table "posts"
repo Example.Repo repo Example.Repo
end end
attributes do attributes do
attribute :text, :string do attribute :text, :string do
allow_nil? false allow_nil? false
end end
attribute :visibility, :atom do attribute :visibility, :atom do
constraints [ constraints [
one_of: [:friends, :everyone] one_of: [:friends, :everyone]
] ]
end end
end end
actions do actions do
update :react do update :react do
argument :type, Example.Types.ReactionType do argument :type, Example.Types.ReactionType do
allow_nil? false allow_nil? false
end end
change manage_relationship( change manage_relationship(
:type, :type,
:reactions, :reactions,
type: :append type: :append
) )
end end
end end
relationships do relationships do
belongs_to :author, Example.User do belongs_to :author, Example.User do
required? true required? true
end end
has_many :reactions, Example.Reaction has_many :reactions, Example.Reaction
end end
end end
""" """
|> to_code()
defp post_example do
@post_example
end end
end end

View file

@ -1,7 +0,0 @@
defmodule AshHqWeb.AuthAccessPipeline do
use Guardian.Plug.Pipeline, otp_app: :ash_hq
plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
plug Guardian.Plug.EnsureAuthenticated
plug Guardian.Plug.LoadResource, allow_blank: true
end

View file

@ -1,11 +0,0 @@
defmodule AshHqWeb.AuthErrorHandler do
import Plug.Conn
@behaviour Guardian.Plug.ErrorHandler
@impl Guardian.Plug.ErrorHandler
def auth_error(conn, {type, _reason}, _opts) do
body = Jason.encode!(%{message: to_string(type)})
send_resp(conn, 401, body)
end
end

View file

@ -9,19 +9,17 @@ defmodule AshHqWeb.Router do
plug :fetch_live_flash plug :fetch_live_flash
plug :put_root_layout, {AshHqWeb.LayoutView, :root} plug :put_root_layout, {AshHqWeb.LayoutView, :root}
plug :protect_from_forgery plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
plug AshHqWeb.SessionPlug plug AshHqWeb.SessionPlug
end end
pipeline :dead_view_authentication do
plug :fetch_current_user
end
pipeline :api do pipeline :api do
plug :accepts, ["json"] plug :accepts, ["json"]
end end
pipeline :api_authenticated do
plug AshHqWeb.AuthAccessPipeline
end
scope "/", AshHqWeb do scope "/", AshHqWeb do
pipe_through :api pipe_through :api
post "/import/:library", ImportController, :import post "/import/:library", ImportController, :import
@ -30,7 +28,9 @@ defmodule AshHqWeb.Router do
scope "/", AshHqWeb do scope "/", AshHqWeb do
pipe_through :browser pipe_through :browser
live_session :main, root_layout: {AshHqWeb.LayoutView, "root.html"} do live_session :main,
on_mount: {AshHqWeb.LiveUserAuth, :live_user},
root_layout: {AshHqWeb.LayoutView, "root.html"} do
live "/", AppViewLive, :home live "/", AppViewLive, :home
live "/docs/", AppViewLive, :docs_dsl live "/docs/", AppViewLive, :docs_dsl
live "/docs/guides/:library/:version/*guide", AppViewLive, :docs_dsl live "/docs/guides/:library/:version/*guide", AppViewLive, :docs_dsl
@ -45,7 +45,12 @@ defmodule AshHqWeb.Router do
## Authentication routes ## Authentication routes
scope "/", AshHqWeb do scope "/", AshHqWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated, :put_session_layout] pipe_through [
:browser,
:dead_view_authentication,
:redirect_if_user_is_authenticated,
:put_session_layout
]
get "/users/register", UserRegistrationController, :new get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create post "/users/register", UserRegistrationController, :create
@ -58,7 +63,7 @@ defmodule AshHqWeb.Router do
end end
scope "/", AshHqWeb do scope "/", AshHqWeb do
pipe_through [:browser, :require_authenticated_user] pipe_through [:browser, :dead_view_authentication, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update put "/users/settings", UserSettingsController, :update
@ -66,9 +71,9 @@ defmodule AshHqWeb.Router do
end end
scope "/", AshHqWeb do scope "/", AshHqWeb do
pipe_through [:browser] pipe_through [:browser, :dead_view_authentication]
get "/users/log_out", UserSessionController, :delete # get "/users/log_out", UserSessionController, :delete
delete "/users/log_out", UserSessionController, :delete delete "/users/log_out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create post "/users/confirm", UserConfirmationController, :create
@ -91,7 +96,7 @@ defmodule AshHqWeb.Router do
import Phoenix.LiveDashboard.Router import Phoenix.LiveDashboard.Router
scope "/" do scope "/" do
pipe_through :browser pipe_through [:browser, :dead_view_authentication]
live_dashboard "/dashboard", metrics: AshHqWeb.Telemetry live_dashboard "/dashboard", metrics: AshHqWeb.Telemetry
end end
@ -103,7 +108,7 @@ defmodule AshHqWeb.Router do
# node running the Phoenix server. # node running the Phoenix server.
if Mix.env() == :dev do if Mix.env() == :dev do
scope "/dev" do scope "/dev" do
pipe_through :browser pipe_through [:browser, :dead_view_authentication]
forward "/mailbox", Plug.Swoosh.MailboxPreview forward "/mailbox", Plug.Swoosh.MailboxPreview
end end

View file

@ -1,4 +1,5 @@
defmodule AshHqWeb.Telemetry do defmodule AshHqWeb.Telemetry do
@moduledoc "Telemetry metrics registry/handler"
use Supervisor use Supervisor
import Telemetry.Metrics import Telemetry.Metrics

View file

@ -1,13 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class={"h-full #{@configured_theme}"}> <html lang="en" class="<%= "h-full #{@configured_theme}" %>">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "Ash Framework" %> <%= live_title_tag assigns[:page_title] || "Ash Framework" %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/> <link nonce="<%= @script_src_nonce %>" phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/assets/app.css") %>"/>
<script> <script nonce="<%= @script_src_nonce %>" >
const configuredThemeRow = document.cookie const configuredThemeRow = document.cookie
.split('; ') .split('; ')
.find(row => row.startsWith('theme=')) .find(row => row.startsWith('theme='))
@ -33,8 +33,8 @@
</head> </head>
<body class="h-full"> <body class="h-full">
<%= @inner_content %> <%= @inner_content %>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> <script nonce="<%= @script_src_nonce %>" src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>mermaid.init(".mermaid")</script> <script nonce="<%= @script_src_nonce %>">mermaid.init(".mermaid")</script>
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script> <script nonce="<%= @script_src_nonce %>" defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/assets/app.js") %>"></script>
</body> </body>
</html> </html>

View file

@ -5,9 +5,9 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "AshHq", suffix: " · Phoenix Framework" %> <%= live_title_tag assigns[:page_title] || "AshHq" %>
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/assets/app.css") %>"/> <link nonce="<%= @script_src_nonce %>" phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/assets/app.css") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/assets/app.js") %>"></script> <script nonce="<%= @script_src_nonce %>" defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/assets/app.js") %>"></script>
</head> </head>
<body class="flex flex-col h-full bg-gray-700"> <body class="flex flex-col h-full bg-gray-700">
<header> <header>

View file

@ -1,4 +1,8 @@
defmodule AshHqWeb.UserAuth do defmodule AshHqWeb.UserAuth do
@moduledoc """
Helpers for authenticating, logging in and logging out users.
"""
import Plug.Conn import Plug.Conn
import Phoenix.Controller import Phoenix.Controller
@ -106,14 +110,20 @@ defmodule AshHqWeb.UserAuth do
def fetch_current_user(conn, _opts) do def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn) {user_token, conn} = ensure_user_token(conn)
user = assign(conn, :current_user, user_for_session_token(user_token))
if user_token do end
AshHq.Accounts.User
|> Ash.Query.for_read(:by_token, token: user_token, context: "session")
|> AshHq.Accounts.read_one!()
end
assign(conn, :current_user, user) @doc """
Gets the user corresponding to a given session token.
If the session token is nil or does not exist, then `nil` is returned.
"""
def user_for_session_token(nil), do: nil
def user_for_session_token(user_token) do
AshHq.Accounts.User
|> Ash.Query.for_read(:by_token, token: user_token, context: "session")
|> AshHq.Accounts.read_one!()
end end
defp ensure_user_token(conn) do defp ensure_user_token(conn) do

View file

@ -5,7 +5,9 @@ defmodule AshHqWeb.AppViewLive do
alias AshHq.Docs.Extensions.RenderMarkdown alias AshHq.Docs.Extensions.RenderMarkdown
alias AshHqWeb.Components.{Search, SearchBar} alias AshHqWeb.Components.{Search, SearchBar}
alias AshHqWeb.Pages.{Docs, Home} alias AshHqWeb.Pages.{Docs, Home}
alias AshHqWeb.Router.Helpers, as: Routes
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
alias Surface.Components.LiveRedirect
require Ash.Query require Ash.Query
data configured_theme, :string, default: :system data configured_theme, :string, default: :system
@ -14,6 +16,7 @@ defmodule AshHqWeb.AppViewLive do
data libraries, :list, default: [] data libraries, :list, default: []
data selected_types, :map, default: %{} data selected_types, :map, default: %{}
data sidebar_state, :map, default: %{} data sidebar_state, :map, default: %{}
data current_user, :map
data library, :any, default: nil data library, :any, default: nil
data extension, :any, default: nil data extension, :any, default: nil
@ -130,6 +133,11 @@ defmodule AshHqWeb.AppViewLive do
<Heroicons.Solid.MoonIcon class="w-6 h-6 fill-gray-400 hover:fill-gray-200 hover:text-gray-200" /> <Heroicons.Solid.MoonIcon class="w-6 h-6 fill-gray-400 hover:fill-gray-200 hover:text-gray-200" />
{/case} {/case}
</button> </button>
{#if @current_user}
<button class="flex flex-row space-x-2 items-center" phx-click={toggle_account_dropdown()}> <div>Account</div> <Heroicons.Solid.ChevronDownIcon class="w-4 h-4" /></button>
{#else}
<LiveRedirect to={Routes.user_session_path(AshHqWeb.Endpoint, :create)} >Sign In </LiveRedirect>
{/if}
</div> </div>
</div> </div>
{#case @live_action} {#case @live_action}
@ -161,6 +169,23 @@ defmodule AshHqWeb.AppViewLive do
""" """
end end
defp toggle_account_dropdown(js \\ %JS{}) do
js
|> JS.toggle(
to: "#account-dropdown",
in: {
"transition ease-in duration-100",
"opacity-0",
"opacity-100"
},
out: {
"transition ease-out duration-75",
"opacity-100",
"opacity-0"
}
)
end
def handle_params(params, uri, socket) do def handle_params(params, uri, socket) do
{:noreply, {:noreply,
socket socket

17
mix.exs
View file

@ -56,8 +56,9 @@ defmodule AshHq.MixProject do
{:swoosh, "~> 1.3"}, {:swoosh, "~> 1.3"},
{:premailex, "~> 0.3.0"}, {:premailex, "~> 0.3.0"},
# Authentication # Authentication
{:guardian, "~> 2.0"},
{:bcrypt_elixir, "~> 3.0"}, {:bcrypt_elixir, "~> 3.0"},
# CSP
{:plug_content_security_policy, "~> 0.2.1"},
# Phoenix/Core dependencies # Phoenix/Core dependencies
{:phoenix, "~> 1.6.6"}, {:phoenix, "~> 1.6.6"},
{:phoenix_ecto, "~> 4.4"}, {:phoenix_ecto, "~> 4.4"},
@ -77,9 +78,15 @@ defmodule AshHq.MixProject do
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"}, {:plug_cowboy, "~> 2.5"},
# Dependencies # Dependencies
{:sobelow, "~> 0.8", only: :dev}, {:elixir_sense, github: "elixir-lsp/elixir_sense"},
{:credo, "~> 1.4", only: [:dev, :test], runtime: false}, # Build/Check dependencies
{:elixir_sense, github: "elixir-lsp/elixir_sense"} {:git_ops, "~> 2.4.4", only: :dev},
{:ex_doc, "~> 0.23", only: :dev, runtime: false},
{:ex_check, "~> 0.14", only: :dev},
{:credo, ">= 0.0.0", only: :dev, runtime: false},
{:dialyxir, ">= 0.0.0", only: :dev, runtime: false},
{:sobelow, ">= 0.0.0", only: :dev, runtime: false},
{:excoveralls, "~> 0.14", only: [:dev, :test]}
] ]
end end
@ -94,8 +101,10 @@ defmodule AshHq.MixProject do
seed: ["run priv/repo/seeds.exs"], seed: ["run priv/repo/seeds.exs"],
setup: ["ash_postgres.create", "ash_postgres.migrate", "seed"], setup: ["ash_postgres.create", "ash_postgres.migrate", "seed"],
reset: ["drop", "setup"], reset: ["drop", "setup"],
credo: "credo --strict",
drop: ["ash_postgres.drop"], drop: ["ash_postgres.drop"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
sobelow: ["sobelow --skip"],
"assets.deploy": [ "assets.deploy": [
"cmd --cd assets npm run deploy", "cmd --cd assets npm run deploy",
"esbuild default --minify", "esbuild default --minify",

View file

@ -2,9 +2,6 @@
"ash": {:git, "https://github.com/ash-project/ash.git", "fe12f40056661e84e702b3fb50badef1d9f3c99f", []}, "ash": {:git, "https://github.com/ash-project/ash.git", "fe12f40056661e84e702b3fb50badef1d9f3c99f", []},
"ash_phoenix": {:git, "https://github.com/ash-project/ash_phoenix.git", "538784765f5c38cde1b9b527aa348b62d625c01f", []}, "ash_phoenix": {:git, "https://github.com/ash-project/ash_phoenix.git", "538784765f5c38cde1b9b527aa348b62d625c01f", []},
"ash_postgres": {:git, "https://github.com/ash-project/ash_postgres.git", "e20e68e73af334dec540786b9275fcdf0cb86731", []}, "ash_postgres": {:git, "https://github.com/ash-project/ash_postgres.git", "e20e68e73af334dec540786b9275fcdf0cb86731", []},
"bamboo": {:hex, :bamboo, "2.2.0", "f10a406d2b7f5123eb1f02edfa043c259db04b47ab956041f279eaac776ef5ce", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8c3b14ba7d2f40cb4be04128ed1e2aff06d91d9413d38bafb4afccffa3ade4fc"},
"bamboo_phoenix": {:hex, :bamboo_phoenix, "1.0.0", "f3cc591ffb163ed0bf935d256f1f4645cd870cf436545601215745fb9cc9953f", [:mix], [{:bamboo, ">= 2.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.3.0", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "6db88fbb26019c84a47994bb2bd879c0887c29ce6c559bc6385fd54eb8b37dee"},
"bamboo_postmark": {:hex, :bamboo_postmark, "1.0.0", "37e3dea3d06b79a17b6b98ef9261f8f4488619c6283f19306f93d3b636d6f9fb", [:mix], [{:bamboo, ">= 2.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:hackney, ">= 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "443b3fb9e00a5d092ccfc91cfe3dbecab2a931114d4dc5e1e70f28f6c640c63d"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"castore": {:hex, :castore, "0.1.17", "ba672681de4e51ed8ec1f74ed624d104c0db72742ea1a5e74edbc770c815182f", [:mix], [], "hexpm", "d9844227ed52d26e7519224525cb6868650c272d4a3d327ce3ca5570c12163f9"}, "castore": {:hex, :castore, "0.1.17", "ba672681de4e51ed8ec1f74ed624d104c0db72742ea1a5e74edbc770c815182f", [:mix], [], "hexpm", "d9844227ed52d26e7519224525cb6868650c272d4a3d327ce3ca5570c12163f9"},
@ -19,29 +16,32 @@
"credo": {:hex, :credo, "1.6.6", "f51f8d45db1af3b2e2f7bee3e6d3c871737bda4a91bff00c5eec276517d1a19c", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "625520ce0984ee0f9f1f198165cd46fa73c1e59a17ebc520038b8fce056a5bdc"}, "credo": {:hex, :credo, "1.6.6", "f51f8d45db1af3b2e2f7bee3e6d3c871737bda4a91bff00c5eec276517d1a19c", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "625520ce0984ee0f9f1f198165cd46fa73c1e59a17ebc520038b8fce056a5bdc"},
"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"}, "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"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
"docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"},
"earmark": {:hex, :earmark, "1.5.0-pre1", "e04aca73692bc3cda3429d6df99c8dae2bf76411e5e76d006a4bc04ac81ef1c1", [:mix], [{:earmark_parser, "~> 1.4.21", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "26ec0473ad2ef995b9672f89309a7a4952887f69b78cfc7af14e320bc6546bfa"}, "earmark": {:hex, :earmark, "1.5.0-pre1", "e04aca73692bc3cda3429d6df99c8dae2bf76411e5e76d006a4bc04ac81ef1c1", [:mix], [{:earmark_parser, "~> 1.4.21", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "26ec0473ad2ef995b9672f89309a7a4952887f69b78cfc7af14e320bc6546bfa"},
"earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"},
"ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [: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", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"}, "ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [: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", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"},
"ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 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", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"}, "ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 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", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"},
"elasticlunr": {:hex, :elasticlunr, "0.6.6", "937a41a7293040e060f880817abac8e025ac9e146554e24042aaf8fbe94a0d1f", [:mix], [{:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: false]}, {:stemmer, "~> 1.0", [hex: :stemmer, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "d02244cb10c46b82bbc1e68477be296aa78f2d6ecf70355722ce916ff24f6958"},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "6e3334406c1dca8d1809cd9d64a2b1a7888c56d3", []}, "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "6e3334406c1dca8d1809cd9d64a2b1a7888c56d3", []},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"esbuild": {:hex, :esbuild, "0.4.0", "9f17db148aead4cf1e6e6a584214357287a93407b5fb51a031f122b61385d4c2", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "b61e4e6b92ffe45e4ee4755a22de6211a67c67987dc02afb35a425a0add1d447"}, "esbuild": {:hex, :esbuild, "0.4.0", "9f17db148aead4cf1e6e6a584214357287a93407b5fb51a031f122b61385d4c2", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "b61e4e6b92ffe45e4ee4755a22de6211a67c67987dc02afb35a425a0add1d447"},
"ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"}, "ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"},
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
"ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [: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", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"},
"excoveralls": {:hex, :excoveralls, "0.14.6", "610e921e25b180a8538229ef547957f7e04bd3d3e9a55c7c5b7d24354abbba70", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "0eceddaa9785cfcefbf3cd37812705f9d8ad34a758e513bb975b081dce4eb11e"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.10.2", "9ad27d68270d879f73f26604bb2e573d40f29bf0e907064a9a337f90a16a0312", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dd8b11b282072cec2ef30852283949c248bd5d2820c88d8acc89402b81db7550"}, "finch": {:hex, :finch, "0.10.2", "9ad27d68270d879f73f26604bb2e573d40f29bf0e907064a9a337f90a16a0312", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dd8b11b282072cec2ef30852283949c248bd5d2820c88d8acc89402b81db7550"},
"floki": {:hex, :floki, "0.32.1", "dfe3b8db3b793939c264e6f785bca01753d17318d144bd44b407fb3493acaa87", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "d4b91c713e4a784a3f7b1e3cc016eefc619f6b1c3898464222867cafd3c681a3"}, "floki": {:hex, :floki, "0.32.1", "dfe3b8db3b793939c264e6f785bca01753d17318d144bd44b407fb3493acaa87", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "d4b91c713e4a784a3f7b1e3cc016eefc619f6b1c3898464222867cafd3c681a3"},
"getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"},
"gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"}, "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
"guardian": {:hex, :guardian, "2.2.4", "3dafdc19665411c96b2796d184064d691bc08813a132da5119e39302a252b755", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "6f83d4309c16ec2469da8606bb2a9815512cc2fac1595ad34b79940a224eb110"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.4.5", "185a724dfde3745edd22f7571d59c47a835cf54ded67e9ccbc951920b7eec4c2", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e323a5b01ad53bc8c19c3a444be3e61ed7803ecd2e95530446ae9327d0143ecc"},
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"}, "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
"jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"},
"kino": {:hex, :kino, "0.6.2", "3e8463ea19551f368c3dcbbf39d36b2627a33916598bfe87f51adc9aaab453fb", [:mix], [{:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "488cd83fa6efcdb4d5289c25daf842c44b33508fea048eb98f58132afc4ed513"},
"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": {: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_eex": {:hex, :makeup_eex, "0.1.1", "89352d5da318d97ae27bbcc87201f274504d2b71ede58ca366af6a5fbed9508d", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d111a0994eaaab09ef1a4b3b313ef806513bb4652152c26c0d7ca2be8402a964"}, "makeup_eex": {:hex, :makeup_eex, "0.1.1", "89352d5da318d97ae27bbcc87201f274504d2b71ede58ca366af6a5fbed9508d", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d111a0994eaaab09ef1a4b3b313ef806513bb4652152c26c0d7ca2be8402a964"},
"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_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"},
@ -68,22 +68,20 @@
"phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.1", "407dcb90755167fd9e3311b60565ff32ed0d234010363406c07cdb4175b95bc5", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "68f4bdb2ac3b594209e54625d3d58c9e2e98b90f2ec8e03235f66e88c9eda5fe"}, "picosat_elixir": {:hex, :picosat_elixir, "0.2.1", "407dcb90755167fd9e3311b60565ff32ed0d234010363406c07cdb4175b95bc5", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "68f4bdb2ac3b594209e54625d3d58c9e2e98b90f2ec8e03235f66e88c9eda5fe"},
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
"plug_content_security_policy": {:hex, :plug_content_security_policy, "0.2.1", "0a19c76307ad000b3757739c14b34b83ecccf7d0a3472e64e14797a20b62939b", [:mix], [{:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ceea10050671c0387c64526e2cb337ee08e12705c737eaed80439266df5b2e29"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"postgrex": {:hex, :postgrex, "0.16.3", "fac79a81a9a234b11c44235a4494d8565303fa4b9147acf57e48978a074971db", [: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", "aeaae1d2d1322da4e5fe90d241b0a564ce03a3add09d7270fb85362166194590"}, "postgrex": {:hex, :postgrex, "0.16.3", "fac79a81a9a234b11c44235a4494d8565303fa4b9147acf57e48978a074971db", [: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", "aeaae1d2d1322da4e5fe90d241b0a564ce03a3add09d7270fb85362166194590"},
"premailex": {:hex, :premailex, "0.3.16", "25c0c9c969f0025bbfdb06834f8f0fbd46e5ec50f5c252e6492165802ffbd2a6", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:floki, "~> 0.19", [hex: :floki, repo: "hexpm", optional: false]}, {:meeseeks, "~> 0.11", [hex: :meeseeks, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "c6b042f89ca63025dfbe3ef54fdbbe9d5f043b7c33d8e58f43a41d13a9475111"}, "premailex": {:hex, :premailex, "0.3.16", "25c0c9c969f0025bbfdb06834f8f0fbd46e5ec50f5c252e6492165802ffbd2a6", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:floki, "~> 0.19", [hex: :floki, repo: "hexpm", optional: false]}, {:meeseeks, "~> 0.11", [hex: :meeseeks, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "c6b042f89ca63025dfbe3ef54fdbbe9d5f043b7c33d8e58f43a41d13a9475111"},
"providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"}, "providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"req": {:hex, :req, "0.2.1", "5d4ee7bc6666cd4d77e95f89ce75ca0ca73b6a25eeebbe2e7bc60cdd56d73865", [:mix], [{:finch, "~> 0.9.1", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}], "hexpm", "ababd5c8a334848bde2bc3c2f518df22211c8533d863d15bfefa04796abc3633"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"sourceror": {:hex, :sourceror, "0.11.1", "1b80efe84330beefb6b3da95b75c1e1cdefe9dc785bf4c5064fae251a8af615c", [:mix], [], "hexpm", "22b6828ee5572f6cec75cc6357f3ca6c730a02954cef0302c428b3dba31e5e74"}, "sourceror": {:hex, :sourceror, "0.11.1", "1b80efe84330beefb6b3da95b75c1e1cdefe9dc785bf4c5064fae251a8af615c", [:mix], [], "hexpm", "22b6828ee5572f6cec75cc6357f3ca6c730a02954cef0302c428b3dba31e5e74"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"stemmer": {:hex, :stemmer, "1.1.0", "71221331ced40832b47e6989a12dd9de1b15c982043d1014742be83c34ec9e79", [:mix], [], "hexpm", "0cb5faf73476b84500e371ff39fd9a494f60ab31d991689c1cd53b920556228f"},
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
"surface": {:hex, :surface, "0.7.4", "ce9cf98a11e6572008d82b6dd1dd25fd90966d69cc72a06d69058ef3e7063df8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.4", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.9", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "052c2a9a35e260339ec0f9bbc667224993e7e2805c36409736f673ffe7d486ac"}, "surface": {:hex, :surface, "0.7.4", "ce9cf98a11e6572008d82b6dd1dd25fd90966d69cc72a06d69058ef3e7063df8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.4", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.9", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "052c2a9a35e260339ec0f9bbc667224993e7e2805c36409736f673ffe7d486ac"},
"surface_heroicons": {:hex, :surface_heroicons, "0.6.0", "04e171843439d2d52c868f8adf5294c49505f504a74a0200179e49c447d6f354", [:mix], [{:surface, ">= 0.5.0", [hex: :surface, repo: "hexpm", optional: false]}], "hexpm", "1136c88a8de44a63c050cec9b0b64f771127dfd96feabab4cd0bde8b6b727ba2"}, "surface_heroicons": {:hex, :surface_heroicons, "0.6.0", "04e171843439d2d52c868f8adf5294c49505f504a74a0200179e49c447d6f354", [:mix], [{:surface, ">= 0.5.0", [hex: :surface, repo: "hexpm", optional: false]}], "hexpm", "1136c88a8de44a63c050cec9b0b64f771127dfd96feabab4cd0bde8b6b727ba2"},
"swoosh": {:hex, :swoosh, "1.6.6", "6018c6f4659ac0b4f30684982993b7812b2bb97436d39f76fcfa8c9e3ae74f85", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e92c7206efd442f08484993676ab072afab2f2bb1e87e604230bb1183c5980de"}, "swoosh": {:hex, :swoosh, "1.6.6", "6018c6f4659ac0b4f30684982993b7812b2bb97436d39f76fcfa8c9e3ae74f85", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e92c7206efd442f08484993676ab072afab2f2bb1e87e604230bb1183c5980de"},
"table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
@ -91,5 +89,4 @@
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, "typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
} }

View file

@ -0,0 +1,21 @@
defmodule AshHq.Repo.Migrations.MigrateResources19 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:guides) do
remove :sanitized_name
end
end
def down do
alter table(:guides) do
add :sanitized_name, :text, null: false
end
end
end

View file

@ -0,0 +1,132 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "route",
"type": "text"
},
{
"allow_nil?": false,
"default": "fragment(\"uuid_generate_v4()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "order",
"type": "bigint"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": false,
"default": "\"\"",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "text",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "text_html",
"type": "text"
},
{
"allow_nil?": false,
"default": "\"Topics\"",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "category",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": {
"destination_field": "id",
"destination_field_default": null,
"destination_field_generated": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "guides_library_version_id_fkey",
"on_delete": "delete",
"on_update": null,
"schema": "public",
"table": "library_versions"
},
"size": null,
"source": "library_version_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [
{
"code?": false,
"down": "DROP INDEX guides_name_lower_index;",
"name": "name_index",
"up": "CREATE INDEX guides_name_lower_index ON guides(lower(name));\n"
},
{
"code?": false,
"down": "DROP INDEX guides_name_trigram_index;",
"name": "trigram_index",
"up": "CREATE INDEX guides_name_trigram_index ON guides USING GIST (name gist_trgm_ops);\n"
},
{
"code?": false,
"down": "DROP INDEX guides_search_index;",
"name": "search_index",
"up": "CREATE INDEX guides_search_index ON guides USING GIN((\n setweight(to_tsvector('english', name), 'A') ||\n setweight(to_tsvector('english', text), 'D')\n));\n"
}
],
"has_create_action": true,
"hash": "17C8A7CB3BDF7B8B32274969F1E405C358CEDFA18C0D60E1699F0D63E1D54697",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "guides"
}