mirror of
https://github.com/ash-project/ash_hq.git
synced 2024-09-08 13:51:05 +12:00
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:
parent
59e9f1e8df
commit
025b56d1a4
78 changed files with 1037 additions and 632 deletions
19
.check.exs
Normal file
19
.check.exs
Normal 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
209
.credo.exs
Normal 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`.
|
||||
#
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
]
|
||||
]
|
||||
|
|
|
@ -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:)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
defmodule AshHq.Accounts.Emails do
|
||||
@moduledoc """
|
||||
Delivers emails.
|
||||
"""
|
||||
|
||||
import Swoosh.Email
|
||||
|
||||
def deliver_confirmation_instructions(user, url) do
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
defmodule AshHq.Accounts.Registry do
|
||||
@moduledoc false
|
||||
|
||||
use Ash.Registry,
|
||||
extensions: [Ash.Registry.ResourceValidations]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Validations do
|
||||
alias AshHq.Accounts.User.Validations
|
||||
|
||||
def validate_current_password() do
|
||||
{Validations.ValidateCurrentPassword, []}
|
||||
end
|
||||
end
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
defmodule AshHq.Docs do
|
||||
@moduledoc """
|
||||
Handles documentation data.
|
||||
"""
|
||||
use Ash.Api, otp_app: :ash_hq
|
||||
|
||||
execution do
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
defmodule AshHq.Docs.Registry do
|
||||
@moduledoc false
|
||||
use Ash.Registry,
|
||||
extensions: [Ash.Registry.ResourceValidations]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
defmodule AshHqWeb.Components.CalloutText do
|
||||
@moduledoc "Highlights some text on the page"
|
||||
use Surface.Component
|
||||
|
||||
slot default, required: true
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: ""
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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, ".")
|
||||
|
|
17
lib/ash_hq_web/live_user_auth.ex
Normal file
17
lib/ash_hq_web/live_user_auth.ex
Normal 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
|
|
@ -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 ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
defmodule AshHqWeb.Telemetry do
|
||||
@moduledoc "Telemetry metrics registry/handler"
|
||||
use Supervisor
|
||||
import Telemetry.Metrics
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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
17
mix.exs
|
@ -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",
|
||||
|
|
19
mix.lock
19
mix.lock
|
@ -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"},
|
||||
}
|
||||
|
|
21
priv/repo/migrations/20220806220239_migrate_resources19.exs
Normal file
21
priv/repo/migrations/20220806220239_migrate_resources19.exs
Normal 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
|
132
priv/resource_snapshots/repo/guides/20220806220239.json
Normal file
132
priv/resource_snapshots/repo/guides/20220806220239.json
Normal 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"
|
||||
}
|
Loading…
Reference in a new issue