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,
use_path_for_name?: 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
# of this file so it overrides the configuration defined above.
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 :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 :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 :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
@moduledoc """
Handles user and user token related operations/state
"""
use Ash.Api, otp_app: :ash_hq
end

View file

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

View file

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

View file

@ -1,9 +1,10 @@
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
{__MODULE__, []}
end
This corresponds to how many days the token should be considered valid. See `AshHq.Accounts.User.Helpers` for more.
"""
use Ash.Resource.Preparation
def prepare(query, _opts, _) do
Ash.Query.put_context(

View file

@ -1,4 +1,8 @@
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
@hash_algorithm :sha256

View file

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

View file

@ -1,4 +1,9 @@
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
require Ash.Query

View file

@ -1,4 +1,5 @@
defmodule AshHq.Accounts.User.Helpers do
@moduledoc "Contains values used in various places for authentication"
@reset_password_validity_in_days 1
@confirm_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
@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
def prepare(query, _opts, _) do

View file

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

View file

@ -1,4 +1,10 @@
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
@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
@hash_algorithm :sha256
def build_hashed_token() do
{__MODULE__, []}
end
def change(changeset, _opts, _context) do
token = :crypto.strong_rand_bytes(@rand_size)

View file

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

View file

@ -1,32 +1,18 @@
defmodule AshHq.Accounts.UserToken do
use Ash.Resource,
@moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer,
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
defaults [:read]
read :verify_email_token do
argument :token, :url_encoded_binary, allow_nil?: false
argument :context, :string, allow_nil?: false
prepare Preparations.SetHashedToken
prepare Preparations.DetermineDaysForToken
prepare AshHq.Accounts.Preparations.SetHashedToken
prepare AshHq.Accounts.Preparations.DetermineDaysForToken
filter expr(
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 set_attribute(:context, "session")
change Changes.BuildSessionToken
change AshHq.Accounts.UserToken.Changes.BuildSessionToken
end
create :build_email_token do
@ -50,7 +36,7 @@ defmodule AshHq.Accounts.UserToken do
argument :user, :map
change manage_relationship(:user, type: :replace)
change Changes.BuildHashedToken
change AshHq.Accounts.UserToken.Changes.BuildHashedToken
end
end
@ -64,7 +50,26 @@ defmodule AshHq.Accounts.UserToken do
create_timestamp :created_at
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
belongs_to :user, AshHq.Accounts.User
end
resource do
description """
Represents a token allowing a user to log in, reset their password, or confirm their email.
"""
end
end

View file

@ -1,4 +1,8 @@
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
def change(changeset, opts, _) do

View file

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

View file

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

View file

@ -1,4 +1,8 @@
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{
name: :render_markdown,
schema: [

View file

@ -1,4 +1,11 @@
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
alias Ash.Dsl.Transformer
@ -34,7 +41,5 @@ defmodule AshHq.Docs.Extensions.RenderMarkdown.Transformers.AddRenderMarkdownStr
end)
end
def after?(Ash.Resource.Transformers.DefaultAccept), do: true
def after?(Ash.Resource.Transformers.SetPrimaryActions), do: true
def after?(_), do: false
def after?(_), do: true
end

View file

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

View file

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

View file

@ -1,4 +1,10 @@
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
@search %Ash.Dsl.Section{
@ -24,6 +30,12 @@ defmodule AshHq.Docs.Extensions.Search do
doc:
"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: [
type: :atom,
doc:
@ -72,6 +84,7 @@ defmodule AshHq.Docs.Extensions.Search do
Extension.get_opt(resource, [:search], :doc_attribute, nil)
end
# sobelow_skip ["DOS.BinToAtom"]
def sanitized_name_attribute(resource) do
Extension.get_opt(
resource,
@ -81,6 +94,15 @@ defmodule AshHq.Docs.Extensions.Search do
)
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
Extension.get_opt(
resource,

View file

@ -1,4 +1,18 @@
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
import Ash.Filter.TemplateHelpers
require Ash.Query
@ -19,9 +33,9 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
{:ok,
dsl_state
|> add_code_interface()
|> add_sanitized_name(config)
|> add_search_action(config)
|> add_code_interface()
|> add_search_headline_calculation(config)
|> add_name_matches_calculation(config)
|> add_matches_calculation(config)
@ -55,28 +69,39 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
end
defp add_sanitized_name(dsl_state, config) do
dsl_state
|> Transformer.add_entity(
[:attributes],
Transformer.build_entity!(
Ash.Resource.Dsl,
[:attributes],
:attribute,
name: config.sanitized_name_attribute,
type: :string,
allow_nil?: false
dsl_state =
if Ash.Resource.Info.attribute(config.resource, config.sanitized_name_attribute) do
dsl_state
else
Transformer.add_entity(
dsl_state,
[:attributes],
Transformer.build_entity!(
Ash.Resource.Dsl,
[: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)}
)
)
)
|> Transformer.add_entity(
[: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
dsl_state
end
end
defp add_indexes(dsl_state, config) do
@ -275,7 +300,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
calculation:
Ash.Query.expr(
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),
^arg(:query)
)
@ -296,7 +321,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
end
end
defp html_for_argument() do
defp html_for_argument do
Transformer.build_entity!(
Ash.Resource.Dsl,
[:calculations, :calculate],
@ -307,7 +332,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
)
end
defp query_argument() do
defp query_argument do
Transformer.build_entity!(
Ash.Resource.Dsl,
[:calculations, :calculate],
@ -318,7 +343,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
)
end
defp similarity_argument() do
defp similarity_argument do
Transformer.build_entity!(
Ash.Resource.Dsl,
[:calculations, :calculate],
@ -357,7 +382,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
)
end
defp search_arguments() do
defp search_arguments do
[
Transformer.build_entity!(
Ash.Resource.Dsl,
@ -376,7 +401,7 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
]
end
defp search_preparations() do
defp search_preparations do
[
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :read], :prepare,
preparation: AshHq.Extensions.Search.Preparations.LoadSearchData

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.Dsl do
@moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer,
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_attributes doc: :doc_html
end

View file

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

View file

@ -1,22 +1,28 @@
defmodule AshHq.Docs.Function do
@moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer,
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_attributes(doc: :doc_html)
header_ids?(false)
render_attributes doc: :doc_html
header_ids? false
end
search do
doc_attribute :doc
load_for_search([
load_for_search [
:version_name,
:library_name,
:module_name,
:library_id
])
]
type "Code"
@ -28,7 +34,7 @@ defmodule AshHq.Docs.Function do
repo AshHq.Repo
references do
reference(:library_version, on_delete: :delete)
reference :library_version, on_delete: :delete
end
end

View file

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

View file

@ -1,8 +1,13 @@
defmodule AshHq.Docs.Guide do
@moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer,
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_attributes text: :text_html
end
@ -12,6 +17,8 @@ defmodule AshHq.Docs.Guide do
type "Guides"
load_for_search library_version: [:library_name, :library_display_name]
show_docs_on :route
sanitized_name_attribute :route
auto_sanitize_name_attribute?(false)
end
code_interface do
@ -19,12 +26,7 @@ defmodule AshHq.Docs.Guide do
end
actions do
defaults [:read, :update, :destroy]
create :create do
primary? true
allow_nil_input [:route]
end
defaults [:create, :read, :update, :destroy]
end
changes do

View file

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

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.LibraryVersion do
@moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer,
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
name_attribute :version
library_version_attribute :id

View file

@ -1,4 +1,7 @@
defmodule AshHq.Docs.LibraryVersion.Preparations.SortBySortableVersionInstead do
@moduledoc """
Replaces any sort on `version` by a sort on `sortable_version` instead.
"""
use Ash.Resource.Preparation
def prepare(query, _, _) do
@ -6,8 +9,8 @@ defmodule AshHq.Docs.LibraryVersion.Preparations.SortBySortableVersionInstead do
end
defp replace_sort(nil), do: nil
defp replace_sort(:version), do: :version
defp replace_sort({:version, order}), do: {:version, order}
defp replace_sort(:version), do: :sortable_version
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(other), do: other
end

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.Module do
@moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer,
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_attributes doc: :doc_html
end

View file

@ -1,8 +1,14 @@
defmodule AshHq.Docs.Option do
@moduledoc false
use AshHq.Resource,
data_layer: AshPostgres.DataLayer,
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_attributes doc: :doc_html
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,
otp_app: :ash_hq
def installed_extensions() do
def installed_extensions do
["pg_trgm", "uuid-ossp", "citext"]
end
end

View file

@ -1,31 +1,8 @@
defmodule AshHq.Resource do
@moduledoc "AshHq's base resource."
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
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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
defmodule AshHqWeb.Components.DocSidebar do
@moduledoc "The left sidebar of the docs pages"
use Surface.Component
alias AshHqWeb.Routes
alias AshHqWeb.DocRoutes
alias Surface.Components.LiveRedirect
prop class, :css_class, default: ""
@ -29,21 +30,21 @@ defmodule AshHqWeb.Components.DocSidebar do
</div>
{#for {category, guides} <- guides_by_category(@libraries)}
<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")}
<button :on-click={@collapse_sidebar} phx-value-id={"guides-#{Routes.sanitize_name(category)}"} class="flex flex-row items-center">
{#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-#{DocRoutes.sanitize_name(category)}"} class="flex flex-row items-center">
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" /><div>{category}</div>
</button>
{#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>
</button>
{/if}
</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}
<li class="ml-3">
<LiveRedirect
to={Routes.doc_link(guide, @selected_versions)}
to={DocRoutes.doc_link(guide, @selected_versions)}
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",
"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)}
<li class="ml-3">
<LiveRedirect
to={Routes.doc_link(extension, @selected_versions)}
to={DocRoutes.doc_link(extension, @selected_versions)}
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",
"dark:bg-gray-600": @extension && @extension.id == extension.id
@ -111,7 +112,7 @@ defmodule AshHqWeb.Components.DocSidebar do
{#for module <- modules}
<li class="ml-4">
<LiveRedirect
to={Routes.doc_link(module, @selected_versions)}
to={DocRoutes.doc_link(module, @selected_versions)}
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",
"dark:bg-gray-600": @module && @module.id == module.id
@ -173,7 +174,7 @@ defmodule AshHqWeb.Components.DocSidebar do
{/if}
{/if}
<LiveRedirect
to={Routes.doc_link(dsl, @selected_versions)}
to={DocRoutes.doc_link(dsl, @selected_versions)}
class={
"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

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
@moduledoc "The right nav shown for functions in a module."
use Surface.Component
prop(functions, :list, default: [])
prop(module, :string, required: true)
prop functions, :list, default: []
prop module, :string, required: true
def render(assigns) do
~F"""

View file

@ -1,10 +1,11 @@
defmodule AshHqWeb.Components.Search do
@moduledoc "The search overlay modal"
use Surface.LiveComponent
require Ash.Query
alias AshHqWeb.Routes
alias AshHqWeb.Components.CalloutText
alias AshHqWeb.DocRoutes
alias Surface.Components.{Form, LiveRedirect}
alias Surface.Components.Form.{Checkbox, Label, Select}
@ -108,7 +109,7 @@ defmodule AshHqWeb.Components.Search do
defp render_items(assigns, items) do
~F"""
{#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={
"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,
@ -221,7 +222,7 @@ defmodule AshHqWeb.Components.Search do
{:noreply, socket}
item ->
{:noreply, push_redirect(socket, to: Routes.doc_link(item))}
{:noreply, push_redirect(socket, to: DocRoutes.doc_link(item))}
end
end

View file

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

View file

@ -1,8 +1,9 @@
defmodule AshHqWeb.Components.Tag do
@moduledoc "Renders a simple pill style tag"
use Surface.Component
prop(color, :atom, values: [:red])
slot(default)
prop color, :atom, values: [:red]
slot default
def render(assigns) do
~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
"/docs/dsl/#{library.name}/#{name}"
end

View file

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

View file

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

View file

@ -1,9 +1,10 @@
defmodule AshHqWeb.Pages.Home do
@moduledoc "The home page"
use Surface.LiveComponent
alias AshHqWeb.Components.{CalloutText, CodeExample, SearchBar}
prop libraries, :list, default: []
import AshHqWeb.Components.CodeExample, only: [to_code: 1]
def render(assigns) do
~F"""
@ -26,7 +27,7 @@ defmodule AshHqWeb.Pages.Home do
<CodeExample
id="define-a-resource"
class="grow min-w-fit max-w-[1000px]"
text={post_example()}
code={post_example()}
title="Define a resource"
/>
<div class="flex flex-col space-y-8">
@ -34,14 +35,14 @@ defmodule AshHqWeb.Pages.Home do
class="w-auto"
collapsible
id="use-it-programmatically"
text={changeset_example()}
code={changeset_example()}
title="Use it programmatically"
/>
<CodeExample
class="w-auto"
collapsible
id="graphql-interface"
text={graphql_example()}
code={graphql_example()}
title="Add a GraphQL interface"
/>
<CodeExample
@ -49,7 +50,7 @@ defmodule AshHqWeb.Pages.Home do
collapsible
start_collapsed
id="authorization-policies"
text={policies_example()}
code={policies_example()}
title="Add authorization policies"
/>
<CodeExample
@ -57,7 +58,7 @@ defmodule AshHqWeb.Pages.Home do
collapsible
start_collapsed
id="aggregates"
text={aggregate_example()}
code={aggregate_example()}
title="Define aggregates and calculations"
/>
<CodeExample
@ -65,7 +66,7 @@ defmodule AshHqWeb.Pages.Home do
collapsible
start_collapsed
id="pubsub"
text={notifier_example()}
code={notifier_example()}
title="Broadcast changes over Phoenix PubSub"
/>
<CodeExample
@ -73,7 +74,7 @@ defmodule AshHqWeb.Pages.Home do
collapsible
start_collapsed
id="live-view"
text={live_view_example()}
code={live_view_example()}
title="Use it with Phoenix LiveView"
/>
</div>
@ -84,153 +85,170 @@ defmodule AshHqWeb.Pages.Home do
"""
end
defp changeset_example() do
"""
post = Example.Post.create!(%{
text: "Declarative programming is fun!"
})
@changeset_example """
post = Example.Post.create!(%{
text: "Declarative programming is fun!"
})
Example.Post.react!(post, %{type: :like})
Example.Post.react!(post, %{type: :like})
Example.Post
|> Ash.Query.filter(likes > 10)
|> Ash.Query.sort(likes: :desc)
|> Example.read!()
"""
Example.Post
|> Ash.Query.filter(likes > 10)
|> Ash.Query.sort(likes: :desc)
|> Example.read!()
"""
|> to_code()
defp changeset_example do
@changeset_example
end
defp live_view_example() do
"""
def mount(_params, _session, socket) do
form = AshPhoenix.Form.for_create(Example.Post, :create)
@live_view_example """
def mount(_params, _session, socket) do
form = AshPhoenix.Form.for_create(Example.Post, :create)
{:ok, assign(socket, :form, form}}
end
{:ok, assign(socket, :form, form}}
end
def handle_event("validate", %{"form" => input}, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, input)
def handle_event("validate", %{"form" => input}, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, input)
{:ok, assign(socket, :form, form)}
end
{:ok, assign(socket, :form, form)}
end
def handle_event("submit", _, socket) do
case AshPhoenix.Form.submit(socket.assigns.form) do
{:ok, post} ->
{:ok, redirect_to_post(socket, post)}
def handle_event("submit", _, socket) do
case AshPhoenix.Form.submit(socket.assigns.form) do
{:ok, post} ->
{:ok, redirect_to_post(socket, post)}
{:error, form_with_errors} ->
{:noreply, assign(socket, :form, form_with_errors)}
end
end
"""
{:error, form_with_errors} ->
{:noreply, assign(socket, :form, form_with_errors)}
end
end
"""
|> to_code()
defp live_view_example do
@live_view_example
end
defp graphql_example() do
"""
graphql do
type :post
@graphql_example """
graphql do
type :post
queries do
get :get_post, :read
list :feed, :read
end
queries do
get :get_post, :read
list :feed, :read
end
mutations do
create :create_post, :create
update :react_to_post, :react
end
end
"""
mutations do
create :create_post, :create
update :react_to_post, :react
end
end
"""
|> to_code()
defp graphql_example do
@graphql_example
end
defp policies_example() do
"""
policies do
policy action_type(:read) do
authorize_if expr(visibility == :everyone)
authorize_if relates_to_actor_via([:author, :friends])
end
end
"""
@policies_example """
policies do
policy action_type(:read) do
authorize_if expr(visibility == :everyone)
authorize_if relates_to_actor_via([:author, :friends])
end
end
"""
|> to_code()
defp policies_example do
@policies_example
end
defp notifier_example() do
"""
pub_sub do
module ExampleEndpoint
prefix "post"
@notifier_example """
pub_sub do
module ExampleEndpoint
prefix "post"
publish_all :create, ["created"]
publish :react, ["reaction", :id] event: "reaction"
end
"""
publish_all :create, ["created"]
publish :react, ["reaction", :id] event: "reaction"
end
"""
|> to_code()
defp notifier_example do
@notifier_example
end
defp aggregate_example() do
"""
aggregates do
count :likes, :reactions do
filter expr(type == :like)
end
@aggregate_example """
aggregates do
count :likes, :reactions do
filter expr(type == :like)
end
count :dislikes, :reactions do
filter expr(type == :dislike)
end
end
count :dislikes, :reactions do
filter expr(type == :dislike)
end
end
calculations do
calculate :like_ratio, :float do
expr(likes / (likes + dislikes))
end
end
"""
calculations do
calculate :like_ratio, :float do
expr(likes / (likes + dislikes))
end
end
"""
|> to_code()
defp aggregate_example do
@aggregate_example
end
defp post_example() do
"""
defmodule Example.Post do
use AshHq.Resource,
data_layer: AshPostgres.DataLayer
@post_example """
defmodule Example.Post do
use AshHq.Resource,
data_layer: AshPostgres.DataLayer
postgres do
table "posts"
repo Example.Repo
end
postgres do
table "posts"
repo Example.Repo
end
attributes do
attribute :text, :string do
allow_nil? false
end
attributes do
attribute :text, :string do
allow_nil? false
end
attribute :visibility, :atom do
constraints [
one_of: [:friends, :everyone]
]
end
end
attribute :visibility, :atom do
constraints [
one_of: [:friends, :everyone]
]
end
end
actions do
update :react do
argument :type, Example.Types.ReactionType do
allow_nil? false
end
actions do
update :react do
argument :type, Example.Types.ReactionType do
allow_nil? false
end
change manage_relationship(
:type,
:reactions,
type: :append
)
end
end
change manage_relationship(
:type,
:reactions,
type: :append
)
end
end
relationships do
belongs_to :author, Example.User do
required? true
end
relationships do
belongs_to :author, Example.User do
required? true
end
has_many :reactions, Example.Reaction
end
end
"""
has_many :reactions, Example.Reaction
end
end
"""
|> to_code()
defp post_example do
@post_example
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 :put_root_layout, {AshHqWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
plug AshHqWeb.SessionPlug
end
pipeline :dead_view_authentication do
plug :fetch_current_user
end
pipeline :api do
plug :accepts, ["json"]
end
pipeline :api_authenticated do
plug AshHqWeb.AuthAccessPipeline
end
scope "/", AshHqWeb do
pipe_through :api
post "/import/:library", ImportController, :import
@ -30,7 +28,9 @@ defmodule AshHqWeb.Router do
scope "/", AshHqWeb do
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 "/docs/", AppViewLive, :docs_dsl
live "/docs/guides/:library/:version/*guide", AppViewLive, :docs_dsl
@ -45,7 +45,12 @@ defmodule AshHqWeb.Router do
## Authentication routes
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
post "/users/register", UserRegistrationController, :create
@ -58,7 +63,7 @@ defmodule AshHqWeb.Router do
end
scope "/", AshHqWeb do
pipe_through [:browser, :require_authenticated_user]
pipe_through [:browser, :dead_view_authentication, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
@ -66,9 +71,9 @@ defmodule AshHqWeb.Router do
end
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
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
@ -91,7 +96,7 @@ defmodule AshHqWeb.Router do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through :browser
pipe_through [:browser, :dead_view_authentication]
live_dashboard "/dashboard", metrics: AshHqWeb.Telemetry
end
@ -103,7 +108,7 @@ defmodule AshHqWeb.Router do
# node running the Phoenix server.
if Mix.env() == :dev do
scope "/dev" do
pipe_through :browser
pipe_through [:browser, :dead_view_authentication]
forward "/mailbox", Plug.Swoosh.MailboxPreview
end

View file

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

View file

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

View file

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

View file

@ -1,4 +1,8 @@
defmodule AshHqWeb.UserAuth do
@moduledoc """
Helpers for authenticating, logging in and logging out users.
"""
import Plug.Conn
import Phoenix.Controller
@ -106,14 +110,20 @@ defmodule AshHqWeb.UserAuth do
def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
user =
if user_token do
AshHq.Accounts.User
|> Ash.Query.for_read(:by_token, token: user_token, context: "session")
|> AshHq.Accounts.read_one!()
end
assign(conn, :current_user, user_for_session_token(user_token))
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
defp ensure_user_token(conn) do

View file

@ -5,7 +5,9 @@ defmodule AshHqWeb.AppViewLive do
alias AshHq.Docs.Extensions.RenderMarkdown
alias AshHqWeb.Components.{Search, SearchBar}
alias AshHqWeb.Pages.{Docs, Home}
alias AshHqWeb.Router.Helpers, as: Routes
alias Phoenix.LiveView.JS
alias Surface.Components.LiveRedirect
require Ash.Query
data configured_theme, :string, default: :system
@ -14,6 +16,7 @@ defmodule AshHqWeb.AppViewLive do
data libraries, :list, default: []
data selected_types, :map, default: %{}
data sidebar_state, :map, default: %{}
data current_user, :map
data library, :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" />
{/case}
</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>
{#case @live_action}
@ -161,6 +169,23 @@ defmodule AshHqWeb.AppViewLive do
"""
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
{:noreply,
socket

17
mix.exs
View file

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

View file

@ -2,9 +2,6 @@
"ash": {:git, "https://github.com/ash-project/ash.git", "fe12f40056661e84e702b3fb50badef1d9f3c99f", []},
"ash_phoenix": {:git, "https://github.com/ash-project/ash_phoenix.git", "538784765f5c38cde1b9b527aa348b62d625c01f", []},
"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"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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_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"},
"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"},
"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"},
"getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"},
"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"},
"hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
"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"},
"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_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"},
@ -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"},
"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_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_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"},
"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"},
"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"},
"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"},
"stemmer": {:hex, :stemmer, "1.1.0", "71221331ced40832b47e6989a12dd9de1b15c982043d1014742be83c34ec9e79", [:mix], [], "hexpm", "0cb5faf73476b84500e371ff39fd9a494f60ab31d991689c1cd53b920556228f"},
"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_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"},
"table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
"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_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"},
"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"},
"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"
}