mirror of
https://github.com/ash-project/ash_hq.git
synced 2024-09-08 13:51:05 +12:00
improvement: rewrite package search and storage to use sqlite
improvement: clean up a bunch of unused code
This commit is contained in:
parent
3728a9ce17
commit
4a37edd47e
180 changed files with 2886 additions and 2860 deletions
|
@ -4,6 +4,7 @@
|
|||
:phoenix,
|
||||
:ash,
|
||||
:ash_postgres,
|
||||
:ash_sqlite,
|
||||
:ash_graphql,
|
||||
:surface,
|
||||
:ash_admin,
|
||||
|
@ -23,7 +24,6 @@
|
|||
locals_without_parens: [
|
||||
has_name_attribute?: 1,
|
||||
name_attribute: 1,
|
||||
library_version_attribute: 1,
|
||||
load_for_search: 1,
|
||||
doc_attribute: 1,
|
||||
render_attributes: 1,
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -35,6 +35,7 @@ npm-debug.log
|
|||
/assets/node_modules/
|
||||
|
||||
.elixir_ls
|
||||
ash-hq.db*
|
||||
|
||||
/indexes/*
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM hexpm/elixir:1.15.4-erlang-26.0.2-ubuntu-bionic-20230126
|
||||
FROM hexpm/elixir:1.15.4-erlang-26.0.2-ubuntu-focal-20230126
|
||||
# install build dependencies
|
||||
USER root
|
||||
RUN apt-get update
|
||||
|
@ -13,9 +13,13 @@ RUN apt-get install -y g++
|
|||
RUN apt-get install -y make
|
||||
RUN apt-get install -y curl
|
||||
RUN apt-get install -y build-essential
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get install -y esl-erlang
|
||||
RUN apt-get install -y apt-transport-https
|
||||
RUN apt-get install -y ca-certificates
|
||||
RUN apt-get install -y fuse3 libfuse3-dev libglib2.0-dev
|
||||
RUN apt-get install -y sqlite3
|
||||
COPY --from=flyio/litefs:0.5 /usr/local/bin/litefs /usr/local/bin/litefs
|
||||
ENV NODE_MAJOR=16
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
|
@ -45,4 +49,5 @@ COPY ./config/runtime.exs config/runtime.exs
|
|||
COPY ./rel ./rel
|
||||
RUN mix release --overwrite
|
||||
RUN mkdir indexes
|
||||
CMD ["_build/prod/rel/ash_hq/bin/ash_hq", "start"]
|
||||
COPY ./litefs.yml ./litefs.yml
|
||||
ENTRYPOINT litefs mount
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Config
|
||||
|
||||
config :ash_hq,
|
||||
ecto_repos: [AshHq.Repo]
|
||||
ecto_repos: [AshHq.Repo, AshHq.SqliteRepo]
|
||||
|
||||
config :ash, allow_flow: true
|
||||
|
||||
|
@ -27,9 +27,7 @@ config :appsignal, :config, revision: "test-4"
|
|||
config :ash_hq,
|
||||
ash_apis: [
|
||||
AshHq.Accounts,
|
||||
AshHq.Ashley,
|
||||
AshHq.Blog,
|
||||
AshHq.Discord,
|
||||
AshHq.Docs,
|
||||
AshHq.Github,
|
||||
AshHq.MailingList
|
||||
|
@ -88,8 +86,6 @@ config :esbuild,
|
|||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||
]
|
||||
|
||||
config :open_ai, :http_client_impl, AshHq.Ashley.HttpClient
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
|
|
|
@ -22,6 +22,12 @@ config :git_ops,
|
|||
manage_readme_version: "README.md",
|
||||
version_tag_prefix: "v"
|
||||
|
||||
config :ash_hq, AshHq.SqliteRepo,
|
||||
database: Path.join(__DIR__, "../ash-hq.db"),
|
||||
port: 5432,
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10
|
||||
|
||||
config :ash_hq, :show_search_ranking, true
|
||||
|
||||
secret_key_base = "FxKFwVYhDFah3bLLXXqWdpdcLf5e5T1UyVM6XQp7kCt/Reg5yuAEI3upAVDRoP5e"
|
||||
|
|
|
@ -20,6 +20,12 @@ config :ash_hq, :analytics?, true
|
|||
|
||||
config :ash_hq, :download_ua_on_start, true
|
||||
|
||||
if config_env() == :prod do
|
||||
config :ash_hq, AshHq.SqliteRepo,
|
||||
database: "/litefs/db",
|
||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
|
||||
end
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# To get SSL working, you will need to add the `https` key
|
||||
|
|
|
@ -12,9 +12,6 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
|
|||
config :ash_hq, AshHqWeb.Endpoint, server: true
|
||||
end
|
||||
|
||||
config :open_ai,
|
||||
api_key: System.get_env("OPEN_API_API_KEY")
|
||||
|
||||
config :ash_hq, :github,
|
||||
api_key: System.get_env("GITHUB_API_KEY"),
|
||||
client_id: System.get_env("GITHUB_CLIENT_ID"),
|
||||
|
|
|
@ -24,6 +24,10 @@ config :ash_hq, AshHqWeb.Endpoint,
|
|||
secret_key_base: secret_key_base,
|
||||
server: false
|
||||
|
||||
config :ash_hq, AshHq.SqliteRepo,
|
||||
database: Path.join(__DIR__, "../ash-hq#{System.get_env("MIX_TEST_PARTITION")}.db"),
|
||||
pool_size: 10
|
||||
|
||||
config :ash_hq, cloak_key: "J6ED3yBWjlaOW/5byrukZTEryKa++yXWblJuhP91Qq8="
|
||||
|
||||
# In test we don't send emails.
|
||||
|
|
7
fly.toml
7
fly.toml
|
@ -6,12 +6,13 @@ kill_signal = "SIGINT"
|
|||
kill_timeout = 5
|
||||
processes = []
|
||||
|
||||
[deploy]
|
||||
release_command = "_build/prod/rel/ash_hq/bin/ash_hq eval 'AshHq.Release.migrate'"
|
||||
|
||||
[env]
|
||||
RELEASE_COOKIE = "VsipafjUVIYVpiYiljPg6DNZB8XiSnEf4zLi8WOf9bAU0XK7HuHQqA=="
|
||||
|
||||
[mounts]
|
||||
source = "litefs"
|
||||
destination = "/var/lib/litefs"
|
||||
|
||||
[[services]]
|
||||
internal_port = 4000
|
||||
protocol = "tcp"
|
||||
|
|
|
@ -192,10 +192,6 @@ defmodule AshHq.Accounts.User do
|
|||
attribute :shirt_size, :string
|
||||
attribute :github_info, :map
|
||||
|
||||
attribute :ashley_access, :boolean do
|
||||
default false
|
||||
end
|
||||
|
||||
create_timestamp :created_at
|
||||
update_timestamp :updated_at
|
||||
end
|
||||
|
|
|
@ -34,6 +34,7 @@ defmodule AshHq.Application do
|
|||
AshHq.Vault,
|
||||
# Start the Ecto repository
|
||||
AshHq.Repo,
|
||||
AshHq.SqliteRepo,
|
||||
# Start the Telemetry supervisor
|
||||
AshHqWeb.Telemetry,
|
||||
# Start the PubSub system
|
||||
|
@ -42,6 +43,8 @@ defmodule AshHq.Application do
|
|||
AshHqWeb.Endpoint,
|
||||
{AshHq.Docs.Library.Agent, nil},
|
||||
{Cluster.Supervisor, [topologies, [name: AshHq.ClusterSupervisor]]},
|
||||
{Haystack.Storage.ETS, storage: AshHq.Docs.Indexer.storage()},
|
||||
AshHq.Docs.Indexer,
|
||||
AshHq.Github.Monitor
|
||||
# Start a worker by calling: AshHq.Worker.start_link(arg)
|
||||
# {AshHq.Worker, arg}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
defmodule AshHq.Ashley do
|
||||
@moduledoc false
|
||||
use Ash.Api
|
||||
|
||||
resources do
|
||||
registry(AshHq.Ashley.Registry)
|
||||
end
|
||||
|
||||
authorization do
|
||||
authorize(:by_default)
|
||||
end
|
||||
end
|
|
@ -1,51 +0,0 @@
|
|||
defmodule AshHq.Ashley.HttpClient do
|
||||
@moduledoc false
|
||||
|
||||
alias OpenAI.Behaviours.HttpClientBehaviour
|
||||
alias OpenAI.Error
|
||||
|
||||
@behaviour HttpClientBehaviour
|
||||
|
||||
@impl HttpClientBehaviour
|
||||
|
||||
def request(_, _, _, %{stream: true}, _) do
|
||||
{:error,
|
||||
%Error{
|
||||
message: "Streaming server-sent events is not currently supported by this client."
|
||||
}}
|
||||
end
|
||||
|
||||
def request(method, url, headers, params, opts) do
|
||||
case do_request(method, url, headers, params, opts) do
|
||||
{:ok, %Finch.Response{body: body}} -> {:ok, body}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl HttpClientBehaviour
|
||||
def multipart_request(:post, url, headers, multipart, opts) do
|
||||
body_stream = Multipart.body_stream(multipart)
|
||||
content_type = Multipart.content_type(multipart, "multipart/form-data")
|
||||
content_length = Multipart.content_length(multipart)
|
||||
|
||||
headers = [
|
||||
{"Content-Type", content_type},
|
||||
{"Content-Length", to_string(content_length)} | headers
|
||||
]
|
||||
|
||||
request(:post, url, headers, {:stream, body_stream}, opts)
|
||||
end
|
||||
|
||||
defp do_request(method, url, headers, nil, opts) do
|
||||
Finch.build(method, url, headers, nil, opts) |> Finch.request(OpenAI.Finch)
|
||||
end
|
||||
|
||||
defp do_request(:post, url, headers, {:stream, _} = params, opts) do
|
||||
Finch.build(:post, url, headers, params, opts) |> Finch.request(OpenAI.Finch)
|
||||
end
|
||||
|
||||
defp do_request(method, url, headers, params, opts) do
|
||||
Finch.build(method, url, headers, Jason.encode!(params), opts)
|
||||
|> Finch.request(OpenAI.Finch, receive_timeout: :infinity)
|
||||
end
|
||||
end
|
|
@ -1,54 +0,0 @@
|
|||
defmodule AshHq.Ashley.OpenAi do
|
||||
@moduledoc false
|
||||
@open_ai_embed_model "text-embedding-ada-002"
|
||||
@open_ai_chat_model "gpt-4"
|
||||
@message_token_limit 7000
|
||||
|
||||
@dialyzer {:nowarn_function, {:complete, 4}}
|
||||
|
||||
def create_embeddings(embeddings) do
|
||||
OpenAI.Embeddings.create(@open_ai_embed_model, embeddings, user: "ash-hq-importer")
|
||||
end
|
||||
|
||||
def complete(system_message, system_message_tokens, messages, user_email) do
|
||||
OpenAI.Chat.create_completion(
|
||||
@open_ai_chat_model,
|
||||
[
|
||||
%{role: :system, content: system_message}
|
||||
| fit_to_tokens(messages, @message_token_limit - system_message_tokens)
|
||||
],
|
||||
user: user_email,
|
||||
temperature: 0.2
|
||||
)
|
||||
end
|
||||
|
||||
defp fit_to_tokens(messages, remaining) do
|
||||
messages
|
||||
|> Enum.reverse()
|
||||
|> Enum.reduce_while({remaining, []}, fn message, {remaining, stack} ->
|
||||
tokens = tokens(message)
|
||||
|
||||
if tokens <= remaining do
|
||||
{:cont, {remaining - tokens, [message | stack]}}
|
||||
else
|
||||
{:halt, {remaining, stack}}
|
||||
end
|
||||
end)
|
||||
|> elem(1)
|
||||
end
|
||||
|
||||
def tokens(%{content: message}) do
|
||||
tokens(message)
|
||||
end
|
||||
|
||||
def tokens(message) do
|
||||
# Can't link to my python, so I'm just making a conservative estimate
|
||||
# a token is ~4 chars
|
||||
# Tiktoken.CL100K.encode_ordinary(message)
|
||||
|
||||
message
|
||||
|> String.length()
|
||||
|> div(3)
|
||||
|> Kernel.+(4)
|
||||
end
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
defmodule AshHq.Ashley.Pinecone do
|
||||
@moduledoc false
|
||||
@pinecone_opts [
|
||||
environment: "eu-west1-gcp",
|
||||
project: "ba28bca",
|
||||
index: "ash-hq-docs"
|
||||
]
|
||||
|
||||
def client do
|
||||
Pinecone.Client.new(
|
||||
Keyword.put_new(@pinecone_opts, :api_key, System.get_env("PINECONE_API_KEY"))
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
defmodule AshHq.Ashley.Registry do
|
||||
@moduledoc false
|
||||
use Ash.Registry,
|
||||
extensions: [Ash.Registry.ResourceValidations]
|
||||
|
||||
entries do
|
||||
entry AshHq.Ashley.Question
|
||||
entry AshHq.Ashley.Conversation
|
||||
end
|
||||
end
|
|
@ -1,94 +0,0 @@
|
|||
defmodule AshHq.Ashley.Conversation do
|
||||
@moduledoc false
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer]
|
||||
|
||||
@conversation_limit 10
|
||||
|
||||
def conversation_limit, do: @conversation_limit
|
||||
|
||||
actions do
|
||||
defaults [:read, :destroy]
|
||||
|
||||
create :create do
|
||||
accept [:name]
|
||||
end
|
||||
|
||||
update :update do
|
||||
accept [:name]
|
||||
end
|
||||
|
||||
update :ask do
|
||||
transaction? false
|
||||
|
||||
argument :question, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
manual fn changeset, _ ->
|
||||
changeset.data
|
||||
|> AshHq.Ashley.load!(:over_limit)
|
||||
|> Map.get(:over_limit)
|
||||
|> if do
|
||||
Ash.Changeset.add_error(%{changeset | data: %{changeset.data | over_limit: true}},
|
||||
message: "Conversation limit reached",
|
||||
field: :question
|
||||
)
|
||||
else
|
||||
AshHq.Ashley.Question.ask!(changeset.argument.question, changeset.data.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
attribute :name, :string
|
||||
end
|
||||
|
||||
relationships do
|
||||
has_many :questions, AshHq.Ashley.Question
|
||||
|
||||
belongs_to :user, AshHq.Accounts.User do
|
||||
api AshHq.Accounts
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
policy action_type(:create) do
|
||||
authorize_if relating_to_actor(:user)
|
||||
end
|
||||
|
||||
policy action_type([:read, :update, :destroy]) do
|
||||
authorize_if relates_to_actor_via(:user)
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "conversations"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Ashley
|
||||
define :create, args: [:name]
|
||||
define :read
|
||||
define :destroy
|
||||
end
|
||||
|
||||
changes do
|
||||
change relate_actor(:user), on: [:create]
|
||||
end
|
||||
|
||||
aggregates do
|
||||
count :question_count, [:questions] do
|
||||
filter expr(success)
|
||||
end
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate :over_limit, :boolean, expr(question_count > ^@conversation_limit)
|
||||
end
|
||||
end
|
|
@ -1,192 +0,0 @@
|
|||
defmodule AshHq.Ashley.Question.Actions.Ask do
|
||||
@moduledoc false
|
||||
use Ash.Resource.ManualCreate
|
||||
|
||||
require Logger
|
||||
|
||||
@dialyzer {:nowarn_function, {:create, 3}}
|
||||
@dialyzer {:nowarn_function, {:sources, 1}}
|
||||
|
||||
@system_message_limit 4000
|
||||
|
||||
@static_context """
|
||||
You are an assistant for helping users find relevant documentation about the Ash Framework for the programming language Elixir.
|
||||
Above all else, you should provide links to relevant documentation. If you don’t know the answer, do not make things up, and instead say, “Sorry, I’m not sure about that.”
|
||||
|
||||
Use the following context from our documentation for your answer. All answers should be based on the documentation provided only.
|
||||
|
||||
Example Resource:
|
||||
defmodule Post do
|
||||
use Ash.Resource
|
||||
|
||||
actions do
|
||||
defaults [:read, :update, :destroy]
|
||||
|
||||
create :create do
|
||||
accept [:text]
|
||||
change {Slugify, field: text}
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
attribute :text, :string, allow_nil?: false
|
||||
attribute :slug, :string, allow_nil?: false
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :author, User
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
@system_message_tokens AshHq.Ashley.OpenAi.tokens(@static_context)
|
||||
|
||||
def create(changeset, _, %{actor: actor}) do
|
||||
question = Ash.Changeset.get_attribute(changeset, :question)
|
||||
|
||||
{:ok, %{"data" => [%{"embedding" => vector} | _]}} =
|
||||
AshHq.Ashley.OpenAi.create_embeddings([question])
|
||||
|
||||
{prompt, sources, system_message, system_message_tokens} =
|
||||
AshHq.Ashley.Pinecone.client()
|
||||
|> Pinecone.Vector.query(%{
|
||||
vector: vector,
|
||||
topK: 10,
|
||||
includeMetadata: true,
|
||||
includeValues: true
|
||||
})
|
||||
|> case do
|
||||
{:ok,
|
||||
%{
|
||||
"matches" => []
|
||||
}} ->
|
||||
{
|
||||
%{
|
||||
role: :user,
|
||||
content: question
|
||||
},
|
||||
[],
|
||||
@static_context,
|
||||
@system_message_tokens
|
||||
}
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
"matches" => matches
|
||||
}} ->
|
||||
# This is inefficient
|
||||
context =
|
||||
Enum.map_join(
|
||||
matches,
|
||||
"\n",
|
||||
&"""
|
||||
<a href="https://ash-hq.org/#{&1["metadata"]["link"]}>#{&1["metadata"]["name"]}</a>:
|
||||
#{&1["metadata"]["text"]}
|
||||
"""
|
||||
)
|
||||
|
||||
system_message =
|
||||
"""
|
||||
#{@static_context}
|
||||
|
||||
#{context}
|
||||
"""
|
||||
|> String.slice(0..@system_message_limit)
|
||||
|
||||
{
|
||||
%{
|
||||
role: :user,
|
||||
content: question
|
||||
},
|
||||
matches,
|
||||
system_message,
|
||||
AshHq.Ashley.OpenAi.tokens(system_message)
|
||||
}
|
||||
end
|
||||
|
||||
conversation_id = Ash.Changeset.get_attribute(changeset, :conversation_id)
|
||||
|
||||
conversation_messages =
|
||||
AshHq.Ashley.Question.history!(
|
||||
actor.id,
|
||||
conversation_id,
|
||||
query: Ash.Query.select(AshHq.Ashley.Question, [:question, :answer]),
|
||||
actor: actor
|
||||
)
|
||||
|> Enum.flat_map(fn message ->
|
||||
[
|
||||
%{
|
||||
role: :user,
|
||||
content: message.question
|
||||
},
|
||||
%{
|
||||
role: :assistant,
|
||||
content: message.answer
|
||||
}
|
||||
]
|
||||
end)
|
||||
|
||||
case AshHq.Ashley.OpenAi.complete(
|
||||
system_message,
|
||||
system_message_tokens,
|
||||
conversation_messages ++ [prompt],
|
||||
actor.email
|
||||
) do
|
||||
{:ok,
|
||||
%{
|
||||
"choices" => [
|
||||
%{
|
||||
"message" => %{
|
||||
"content" => answer
|
||||
}
|
||||
}
|
||||
| _
|
||||
]
|
||||
}} ->
|
||||
answer = """
|
||||
#{answer}
|
||||
"""
|
||||
|
||||
AshHq.Ashley.Question.create(
|
||||
conversation_id,
|
||||
actor.id,
|
||||
question,
|
||||
answer,
|
||||
true,
|
||||
sources(sources),
|
||||
authorize?: false
|
||||
)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("""
|
||||
Something went wrong creating a completion
|
||||
|
||||
#{Exception.message(error)}
|
||||
""")
|
||||
|
||||
AshHq.Ashley.Question.create(
|
||||
conversation_id,
|
||||
actor.id,
|
||||
question,
|
||||
"Something went wrong",
|
||||
false,
|
||||
sources(sources),
|
||||
authorize?: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp sources([]), do: ""
|
||||
|
||||
defp sources(sources) do
|
||||
sources
|
||||
|> Enum.map(
|
||||
&%{
|
||||
link: &1["metadata"]["link"],
|
||||
name: &1["metadata"]["name"]
|
||||
}
|
||||
)
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
defmodule AshHq.Ashley.Question.Changes.ValidateLimit do
|
||||
@moduledoc false
|
||||
use Ash.Resource.Change
|
||||
|
||||
def change(changeset, opts, %{actor: actor}) do
|
||||
Ash.Changeset.before_action(
|
||||
changeset,
|
||||
fn changeset ->
|
||||
count =
|
||||
AshHq.Ashley.Question
|
||||
|> Ash.Query.for_read(:questions_in_time_frame, %{}, actor: actor)
|
||||
|> AshHq.Ashley.count!()
|
||||
|
||||
if count >= opts[:question_limit] do
|
||||
Ash.Changeset.add_error(changeset, message: "Question Quota Reached", field: :question)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end,
|
||||
prepend?: true
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,154 +0,0 @@
|
|||
defmodule AshHq.Ashley.Question do
|
||||
@moduledoc false
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer],
|
||||
extensions: [AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
@time_frame_hours 24
|
||||
@question_limit 10
|
||||
|
||||
actions do
|
||||
defaults [:read, :destroy]
|
||||
|
||||
read :history do
|
||||
argument :user, :uuid do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
argument :conversation, :uuid do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
prepare build(sort: [inserted_at: :asc])
|
||||
|
||||
filter expr(user_id == ^arg(:user) and conversation_id == ^arg(:conversation))
|
||||
end
|
||||
|
||||
create :ask do
|
||||
transaction? false
|
||||
accept [:question, :conversation_id]
|
||||
allow_nil_input [:conversation_id]
|
||||
|
||||
argument :conversation_name, :string
|
||||
|
||||
change fn changeset, %{actor: actor} ->
|
||||
Ash.Changeset.before_action(changeset, fn changeset ->
|
||||
if Ash.Changeset.get_attribute(changeset, :conversation_id) do
|
||||
changeset
|
||||
else
|
||||
conversation =
|
||||
AshHq.Ashley.Conversation.create!(
|
||||
changeset.arguments[:conversation_name] || "New Conversation",
|
||||
actor: actor
|
||||
)
|
||||
|
||||
Ash.Changeset.force_change_attribute(changeset, :conversation_id, conversation.id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
change fn changeset, _ ->
|
||||
Ash.Changeset.timeout(changeset, :timer.minutes(3))
|
||||
end
|
||||
|
||||
change {AshHq.Ashley.Question.Changes.ValidateLimit, limit: @question_limit}
|
||||
|
||||
manual AshHq.Ashley.Question.Actions.Ask
|
||||
end
|
||||
|
||||
create :create do
|
||||
accept [:conversation_id, :question, :answer, :success, :user_id, :sources]
|
||||
end
|
||||
|
||||
read :questions_in_time_frame do
|
||||
filter expr(
|
||||
user_id == ^actor(:id) and inserted_at >= ago(@time_frame_hours, :hour) and success
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :question, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :answer, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :sources, {:array, AshHq.Ashley.Question.Types.Source} do
|
||||
allow_nil? false
|
||||
default []
|
||||
end
|
||||
|
||||
attribute :answer_html, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :success, :boolean do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes answer: :answer_html
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :user, AshHq.Accounts.User do
|
||||
allow_nil? false
|
||||
api AshHq.Accounts
|
||||
attribute_writable? true
|
||||
end
|
||||
|
||||
belongs_to :conversation, AshHq.Ashley.Conversation do
|
||||
allow_nil? false
|
||||
attribute_writable? true
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
policy always() do
|
||||
authorize_if actor_present()
|
||||
end
|
||||
|
||||
policy action_type(:read) do
|
||||
authorize_if action(:history)
|
||||
authorize_if accessing_from(AshHq.Ashley.Conversation, :questions)
|
||||
authorize_if expr(user_id == ^actor(:id))
|
||||
end
|
||||
|
||||
policy action(:history) do
|
||||
authorize_if expr(^actor(:id) == ^arg(:user))
|
||||
end
|
||||
|
||||
policy action(:create) do
|
||||
forbid_if always()
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "questions"
|
||||
repo AshHq.Repo
|
||||
|
||||
migration_defaults sources: "[]"
|
||||
|
||||
references do
|
||||
reference :conversation, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Ashley
|
||||
|
||||
define :questions_in_time_frame
|
||||
define :ask, args: [:question]
|
||||
define :create, args: [:conversation_id, :user_id, :question, :answer, :success, :sources]
|
||||
define :history, args: [:user, :conversation]
|
||||
end
|
||||
end
|
|
@ -1,34 +0,0 @@
|
|||
defmodule AshHq.Ashley.Question.Types.Source do
|
||||
@moduledoc false
|
||||
use Ash.Resource,
|
||||
data_layer: :embedded
|
||||
|
||||
actions do
|
||||
create :create do
|
||||
primary? true
|
||||
allow_nil_input [:name]
|
||||
|
||||
change fn changeset, _ ->
|
||||
if Ash.Changeset.get_attribute(changeset, :name) do
|
||||
changeset
|
||||
else
|
||||
Ash.Changeset.force_change_attribute(
|
||||
changeset,
|
||||
:name,
|
||||
Ash.Changeset.get_attribute(changeset, :link)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
attribute :link, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,233 +0,0 @@
|
|||
defmodule AshHq.Ashley.Workers.IndexLibraryVersion do
|
||||
@moduledoc false
|
||||
require Ash.Query
|
||||
|
||||
@dialyzer {:nowarn_function, {:delete_vectors, 2}}
|
||||
@dialyzer {:nowarn_function, {:index_all, 0}}
|
||||
@dialyzer {:nowarn_function, {:perform, 1}}
|
||||
@dialyzer {:nowarn_function, {:guides, 1}}
|
||||
@dialyzer {:nowarn_function, {:modules, 1}}
|
||||
@dialyzer {:nowarn_function, {:functions, 1}}
|
||||
@dialyzer {:nowarn_function, {:dsls, 1}}
|
||||
@dialyzer {:nowarn_function, {:options, 1}}
|
||||
@dialyzer {:nowarn_function, {:name, 1}}
|
||||
@dialyzer {:nowarn_function, {:path, 2}}
|
||||
@dialyzer {:nowarn_function, {:format, 2}}
|
||||
|
||||
def index_all do
|
||||
AshHq.Docs.Library.read!(load: :latest_version_id)
|
||||
|> Enum.filter(& &1.latest_version_id)
|
||||
|> Enum.each(&perform(&1.latest_version_id))
|
||||
end
|
||||
|
||||
def perform(id) do
|
||||
pinecone_client = AshHq.Ashley.Pinecone.client()
|
||||
|
||||
library_version = AshHq.Docs.get!(AshHq.Docs.LibraryVersion, id, load: :library)
|
||||
delete_vectors(pinecone_client, library_version)
|
||||
|
||||
guides(library_version)
|
||||
|> Stream.concat(modules(library_version))
|
||||
|> Stream.concat(functions(library_version))
|
||||
|> Stream.concat(dsls(library_version))
|
||||
|> Stream.concat(options(library_version))
|
||||
|> Stream.map(fn item ->
|
||||
{item, format(item, library_version)}
|
||||
end)
|
||||
|> Stream.chunk_every(100)
|
||||
|> Stream.map(fn batch ->
|
||||
case AshHq.Ashley.OpenAi.create_embeddings(Enum.map(batch, &elem(&1, 1))) do
|
||||
{:ok, %{"data" => data}} ->
|
||||
vectors =
|
||||
Enum.zip_with(data, batch, fn %{"embedding" => values}, {item, text} ->
|
||||
%{
|
||||
values: values,
|
||||
id: item.id,
|
||||
metadata: %{
|
||||
library: library_version.library.name,
|
||||
link: "#{path(item, library_version.library.name)}",
|
||||
name: "#{name(item)}",
|
||||
text: text
|
||||
}
|
||||
}
|
||||
end)
|
||||
|
||||
Pinecone.Vector.upsert(pinecone_client, %{vectors: vectors})
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
defp guides(library_version) do
|
||||
AshHq.Docs.Guide
|
||||
|> Ash.Query.filter(library_version_id == ^library_version.id)
|
||||
|> AshHq.Docs.stream()
|
||||
end
|
||||
|
||||
defp modules(library_version) do
|
||||
AshHq.Docs.Module
|
||||
|> Ash.Query.filter(library_version_id == ^library_version.id)
|
||||
|> AshHq.Docs.stream()
|
||||
end
|
||||
|
||||
defp functions(library_version) do
|
||||
AshHq.Docs.Function
|
||||
|> Ash.Query.filter(library_version_id == ^library_version.id)
|
||||
|> Ash.Query.load(:module_name)
|
||||
|> AshHq.Docs.stream()
|
||||
end
|
||||
|
||||
defp dsls(library_version) do
|
||||
AshHq.Docs.Dsl
|
||||
|> Ash.Query.filter(library_version_id == ^library_version.id)
|
||||
|> Ash.Query.load(:extension_target)
|
||||
|> AshHq.Docs.stream()
|
||||
end
|
||||
|
||||
defp options(library_version) do
|
||||
AshHq.Docs.Option
|
||||
|> Ash.Query.filter(library_version_id == ^library_version.id)
|
||||
|> Ash.Query.load(:extension_target)
|
||||
|> AshHq.Docs.stream()
|
||||
end
|
||||
|
||||
defp name(%AshHq.Docs.Option{} = option) do
|
||||
case option.path do
|
||||
[] ->
|
||||
"#{option.extension_target} - #{option.name}"
|
||||
|
||||
path ->
|
||||
"#{option.extension_target} - #{Enum.join(path, ".")} | #{option.name}"
|
||||
end
|
||||
end
|
||||
|
||||
defp name(%AshHq.Docs.Dsl{} = dsl) do
|
||||
case dsl.path do
|
||||
[] ->
|
||||
"#{dsl.extension_target} - #{dsl.name}"
|
||||
|
||||
path ->
|
||||
"#{dsl.extension_target} - #{Enum.join(path ++ [dsl.name], ".")}"
|
||||
end
|
||||
end
|
||||
|
||||
defp name(%AshHq.Docs.Function{} = function) do
|
||||
"#{function.type} - #{function.module_name}.#{function.name}/#{function.arity}"
|
||||
end
|
||||
|
||||
defp name(%AshHq.Docs.Module{} = module) do
|
||||
module.name
|
||||
end
|
||||
|
||||
defp name(%AshHq.Docs.Guide{} = guide) do
|
||||
guide.name
|
||||
end
|
||||
|
||||
defp path(%AshHq.Docs.Option{} = option, _library_name) do
|
||||
"docs/dsl/#{sanitize_name(option.extension_target)}##{String.replace(option.sanitized_path, "/", "-")}-#{sanitize_name(option.name)}"
|
||||
end
|
||||
|
||||
defp path(%AshHq.Docs.Dsl{} = option, _library_name) do
|
||||
"docs/dsl/#{sanitize_name(option.extension_target)}##{String.replace(option.sanitized_path, "/", "-")}"
|
||||
end
|
||||
|
||||
defp path(
|
||||
%AshHq.Docs.Function{
|
||||
sanitized_name: sanitized_name,
|
||||
arity: arity,
|
||||
type: type,
|
||||
module_name: module_name
|
||||
},
|
||||
library_name
|
||||
) do
|
||||
"/docs/module/#{library_name}/latest/#{sanitize_name(module_name)}##{type}-#{sanitized_name}-#{arity}"
|
||||
end
|
||||
|
||||
defp path(
|
||||
%AshHq.Docs.Module{
|
||||
sanitized_name: sanitized_name
|
||||
},
|
||||
library_name
|
||||
) do
|
||||
"/docs/module/#{library_name}/latest/#{sanitized_name}"
|
||||
end
|
||||
|
||||
defp path(
|
||||
%AshHq.Docs.Guide{
|
||||
route: route
|
||||
},
|
||||
library_name
|
||||
) do
|
||||
"/docs/guides/#{library_name}/latest/#{route}"
|
||||
end
|
||||
|
||||
defp format(%AshHq.Docs.Option{} = option, _library_version) do
|
||||
"""
|
||||
DSL Entity: #{Enum.join(option.path ++ [option.name])}
|
||||
Type: #{option.type}
|
||||
Default: #{option.default}
|
||||
#{option.doc}
|
||||
"""
|
||||
end
|
||||
|
||||
defp format(%AshHq.Docs.Dsl{type: :entity} = dsl, _library_version) do
|
||||
"""
|
||||
DSL Entity: #{Enum.join(dsl.path ++ [dsl.name])}
|
||||
#{Enum.map_join(dsl.examples || [], &"```\n#{&1}\n```")}
|
||||
#{dsl.doc}
|
||||
"""
|
||||
end
|
||||
|
||||
defp format(%AshHq.Docs.Dsl{type: :section} = dsl, _library_version) do
|
||||
"""
|
||||
DSL Section: #{Enum.join(dsl.path ++ [dsl.name])}
|
||||
#{Enum.map_join(dsl.examples || [], &"```\n#{&1}\n```")}
|
||||
#{dsl.doc}
|
||||
"""
|
||||
end
|
||||
|
||||
defp format(%AshHq.Docs.Function{} = function, _library_version) do
|
||||
"""
|
||||
#{function.type} #{function.module_name}.#{function.name}/#{function.arity}
|
||||
Types:
|
||||
#{Enum.join(function.heads, "\n")}
|
||||
|
||||
Docs:
|
||||
#{function.doc}
|
||||
"""
|
||||
end
|
||||
|
||||
defp format(%AshHq.Docs.Module{} = module, _library_version) do
|
||||
"""
|
||||
#{module.name}:
|
||||
#{module.doc}
|
||||
"""
|
||||
end
|
||||
|
||||
defp format(%AshHq.Docs.Guide{} = guide, _library_version) do
|
||||
"""
|
||||
#{guide.name}:
|
||||
#{guide.text}
|
||||
"""
|
||||
end
|
||||
|
||||
def sanitize_name(name, allow_forward_slash? \\ false) do
|
||||
if allow_forward_slash? do
|
||||
String.downcase(String.replace(to_string(name), ~r/[^A-Za-z0-9\/_]/, "-"))
|
||||
else
|
||||
String.downcase(String.replace(to_string(name), ~r/[^A-Za-z0-9_]/, "-"))
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_vectors(pinecone_client, library_version) do
|
||||
pinecone_client
|
||||
|> Pinecone.Vector.delete(%{
|
||||
filter: %{
|
||||
library: library_version.library.name
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
defmodule AshHq.Discord do
|
||||
@moduledoc "Discord api import & interactions"
|
||||
use Ash.Api
|
||||
|
||||
resources do
|
||||
registry(AshHq.Discord.Registry)
|
||||
end
|
||||
end
|
|
@ -5,99 +5,33 @@ defmodule AshHq.Discord.Listener do
|
|||
use Nostrum.Consumer
|
||||
|
||||
import Bitwise
|
||||
@all_types AshHq.Docs.Extensions.Search.Types.types() -- ["Forum"]
|
||||
|
||||
@user_id 1_066_406_803_769_933_834
|
||||
@server_id 711_271_361_523_351_632
|
||||
|
||||
def start_link() do
|
||||
Consumer.start_link(__MODULE__)
|
||||
end
|
||||
|
||||
def search_results!(interaction) do
|
||||
search =
|
||||
interaction.data.options
|
||||
|> Enum.find_value(fn option ->
|
||||
if option.name == "search" do
|
||||
option.value
|
||||
end
|
||||
end)
|
||||
item_list = AshHq.Docs.Indexer.search!(search)
|
||||
|
||||
type =
|
||||
interaction.data.options
|
||||
|> Enum.find_value(fn option ->
|
||||
if option.name == "type" do
|
||||
option.value
|
||||
end
|
||||
end)
|
||||
item_list = Enum.take(item_list, 10)
|
||||
|
||||
library =
|
||||
interaction.data.options
|
||||
|> Enum.find_value(fn option ->
|
||||
if option.name == "library" do
|
||||
option.value
|
||||
end
|
||||
end)
|
||||
count =
|
||||
case Enum.count(item_list) do
|
||||
10 ->
|
||||
"the top 10"
|
||||
|
||||
libraries =
|
||||
AshHq.Docs.Library.read!()
|
||||
|> Enum.filter(& &1.latest_version_id)
|
||||
|
||||
library_version_ids =
|
||||
if library do
|
||||
case Enum.find(libraries, &(&1.name == library)) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
library ->
|
||||
[library.latest_version_id]
|
||||
end
|
||||
else
|
||||
Enum.map(libraries, & &1.latest_version_id)
|
||||
other ->
|
||||
"#{other}"
|
||||
end
|
||||
|
||||
input =
|
||||
if type do
|
||||
%{types: [type]}
|
||||
else
|
||||
%{types: @all_types}
|
||||
end
|
||||
"""
|
||||
Found #{count} results for "#{search}":
|
||||
|
||||
%{result: item_list} = AshHq.Docs.Search.run!(search, library_version_ids, input)
|
||||
|
||||
result_type =
|
||||
if type do
|
||||
"#{type} results"
|
||||
else
|
||||
"results"
|
||||
end
|
||||
|
||||
library =
|
||||
if library do
|
||||
"#{library}"
|
||||
else
|
||||
"all libraries"
|
||||
end
|
||||
|
||||
if item_list do
|
||||
item_list = Enum.take(item_list, 10)
|
||||
|
||||
count =
|
||||
case Enum.count(item_list) do
|
||||
10 ->
|
||||
"the top 10"
|
||||
|
||||
other ->
|
||||
"#{other}"
|
||||
end
|
||||
|
||||
"""
|
||||
Found #{count} #{result_type} in #{library} for query "#{search}":
|
||||
|
||||
#{Enum.map_join(item_list, "\n", &render_search_result(&1))}
|
||||
"""
|
||||
else
|
||||
"Something went wrong."
|
||||
end
|
||||
#{Enum.map_join(item_list, "\n", &render_search_result(&1))}
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_search_result(item) do
|
||||
|
@ -161,19 +95,11 @@ defmodule AshHq.Discord.Listener do
|
|||
|
||||
def rebuild do
|
||||
if Application.get_env(:ash_hq, :discord_bot) do
|
||||
libraries =
|
||||
AshHq.Docs.Library.read!()
|
||||
|> Enum.filter(& &1.latest_library_version)
|
||||
|
||||
build_search_action(libraries)
|
||||
build_search_action()
|
||||
end
|
||||
end
|
||||
|
||||
defp build_search_action(libraries) do
|
||||
library_names =
|
||||
libraries
|
||||
|> Enum.map(& &1.name)
|
||||
|
||||
defp build_search_action() do
|
||||
command = %{
|
||||
name: "ash_hq_search",
|
||||
description: "Search AshHq Documentation",
|
||||
|
@ -185,36 +111,6 @@ defmodule AshHq.Discord.Listener do
|
|||
description: "what you want to search for",
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
# ApplicationCommandType::STRING
|
||||
type: 3,
|
||||
name: "type",
|
||||
description: "What type of thing you want to search for. Defaults to everything.",
|
||||
required: false,
|
||||
choices:
|
||||
Enum.map(@all_types, fn type ->
|
||||
%{
|
||||
name: String.downcase(type),
|
||||
description: "Search only for #{String.downcase(type)} items.",
|
||||
value: type
|
||||
}
|
||||
end)
|
||||
},
|
||||
%{
|
||||
# ApplicationCommandType::STRING
|
||||
type: 3,
|
||||
name: "library",
|
||||
description: "Which library you'd like to search. Defaults to all libraries.",
|
||||
required: false,
|
||||
choices:
|
||||
Enum.map(library_names, fn name ->
|
||||
%{
|
||||
name: name,
|
||||
description: "Search only in the #{name} library.",
|
||||
value: name
|
||||
}
|
||||
end)
|
||||
},
|
||||
%{
|
||||
# ApplicationCommandType::Boolean
|
||||
type: 5,
|
||||
|
@ -227,7 +123,7 @@ defmodule AshHq.Discord.Listener do
|
|||
|
||||
Nostrum.Api.create_guild_application_command(
|
||||
@user_id,
|
||||
AshHq.Discord.Poller.server_id(),
|
||||
@server_id,
|
||||
command
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
defmodule AshHq.Discord.Poller do
|
||||
@moduledoc """
|
||||
Every 2 hours, synchronizes all active threads and the 50 most recent archived threads
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
@poll_interval :timer.hours(1)
|
||||
@server_id 711_271_361_523_351_632
|
||||
@archived_thread_lookback 50
|
||||
|
||||
@channels [
|
||||
1_066_222_835_758_014_606,
|
||||
1_066_223_107_922_210_867,
|
||||
1_019_647_368_196_534_283
|
||||
]
|
||||
|
||||
def server_id, do: @server_id
|
||||
|
||||
defmacrop unwrap(value) do
|
||||
quote do
|
||||
case unquote(value) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, error} ->
|
||||
raise Exception.format(:error, error, [])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def start_link(state) do
|
||||
GenServer.start_link(__MODULE__, state, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(_) do
|
||||
send(self(), :poll)
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
def handle_info(:poll, state) do
|
||||
poll()
|
||||
Process.send_after(self(), :poll, @poll_interval)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def poll do
|
||||
for {channel, index} <- Enum.with_index(@channels) do
|
||||
channel
|
||||
|> Nostrum.Api.get_channel!()
|
||||
|> tap(fn channel ->
|
||||
channel
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:order, index)
|
||||
|> AshHq.Discord.Channel.upsert!()
|
||||
end)
|
||||
|> Map.get(:available_tags)
|
||||
|> Enum.each(fn available_tag ->
|
||||
AshHq.Discord.Tag.upsert!(channel, available_tag.id, available_tag.name)
|
||||
end)
|
||||
end
|
||||
|
||||
active =
|
||||
@server_id
|
||||
|> Nostrum.Api.list_guild_threads()
|
||||
|> unwrap()
|
||||
|> Map.get(:threads)
|
||||
|> Stream.filter(fn thread ->
|
||||
thread.parent_id in @channels
|
||||
end)
|
||||
|> Stream.map(fn thread ->
|
||||
%{
|
||||
thread: thread,
|
||||
messages: get_all_channel_messages(thread.id)
|
||||
}
|
||||
end)
|
||||
|
||||
archived =
|
||||
@channels
|
||||
|> Stream.flat_map(fn channel ->
|
||||
channel
|
||||
|> Nostrum.Api.list_public_archived_threads(limit: @archived_thread_lookback)
|
||||
|> unwrap()
|
||||
|> Map.get(:threads)
|
||||
|> Enum.map(fn thread ->
|
||||
messages =
|
||||
thread.id
|
||||
|> get_all_channel_messages()
|
||||
|
||||
%{
|
||||
thread: thread,
|
||||
messages: messages
|
||||
}
|
||||
end)
|
||||
end)
|
||||
|
||||
active
|
||||
|> Stream.concat(archived)
|
||||
|> Enum.reject(fn
|
||||
%{messages: []} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|> Enum.each(fn %{thread: thread, messages: messages} ->
|
||||
try do
|
||||
author =
|
||||
messages
|
||||
|> Enum.min_by(& &1.timestamp, DateTime)
|
||||
|> Map.get(:author)
|
||||
|
||||
thread
|
||||
|> Map.put(:author, author)
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:channel_id, thread.parent_id)
|
||||
|> Map.put(:tags, thread.applied_tags)
|
||||
|> Map.put(:create_timestamp, thread.thread_metadata.create_timestamp)
|
||||
|> Map.put(:messages, Enum.map(messages, &Map.from_struct/1))
|
||||
|> AshHq.Discord.Thread.upsert!()
|
||||
rescue
|
||||
e ->
|
||||
Logger.error(
|
||||
"Failed to import message:\n #{Exception.format(:error, e, __STACKTRACE__)}"
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_all_channel_messages(thread) do
|
||||
Stream.resource(
|
||||
fn ->
|
||||
:all
|
||||
end,
|
||||
fn
|
||||
nil ->
|
||||
{:halt, nil}
|
||||
|
||||
before ->
|
||||
locator =
|
||||
case before do
|
||||
:all ->
|
||||
nil
|
||||
|
||||
before ->
|
||||
{:before, before}
|
||||
end
|
||||
|
||||
messages =
|
||||
if locator do
|
||||
Nostrum.Api.get_channel_messages!(thread, 100, locator)
|
||||
else
|
||||
Nostrum.Api.get_channel_messages!(thread, 100)
|
||||
end
|
||||
|
||||
if Enum.count(messages) == 100 do
|
||||
{messages, List.last(messages).id}
|
||||
else
|
||||
{messages, nil}
|
||||
end
|
||||
end,
|
||||
& &1
|
||||
)
|
||||
|> Stream.map(fn message ->
|
||||
message
|
||||
|> Map.put(:author, message.author.username)
|
||||
|> Map.update!(:reactions, fn reactions ->
|
||||
reactions
|
||||
|> Kernel.||([])
|
||||
# just don't know what this looks like, so removing them
|
||||
|> Enum.reject(&(is_nil(&1.emoji) || &1.emoji == "" || &1.emoji.animated))
|
||||
|> Enum.map(fn %{count: count, emoji: emoji} ->
|
||||
%{emoji: emoji.name, count: count}
|
||||
end)
|
||||
end)
|
||||
|> Map.update!(:attachments, fn attachments ->
|
||||
Enum.map(attachments, &Map.from_struct/1)
|
||||
end)
|
||||
end)
|
||||
|> Enum.to_list()
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
defmodule AshHq.Discord.Registry do
|
||||
@moduledoc false
|
||||
use Ash.Registry,
|
||||
extensions: [Ash.Registry.ResourceValidations]
|
||||
|
||||
entries do
|
||||
entry AshHq.Discord.Attachment
|
||||
entry AshHq.Discord.Channel
|
||||
entry AshHq.Discord.Message
|
||||
entry AshHq.Discord.Reaction
|
||||
entry AshHq.Discord.Tag
|
||||
entry AshHq.Discord.Thread
|
||||
entry AshHq.Discord.ThreadTag
|
||||
end
|
||||
end
|
|
@ -1,35 +0,0 @@
|
|||
defmodule AshHq.Discord.Attachment do
|
||||
@moduledoc "A discord attachment on a message"
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
end
|
||||
|
||||
attributes do
|
||||
integer_primary_key :id, generated?: false, writable?: true
|
||||
attribute :filename, :string
|
||||
attribute :size, :integer
|
||||
attribute :url, :string
|
||||
attribute :proxy_url, :string
|
||||
attribute :height, :integer
|
||||
attribute :width, :integer
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :message, AshHq.Discord.Message do
|
||||
allow_nil? false
|
||||
attribute_type :integer
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "discord_attachments"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :message, on_delete: :delete, on_update: :update
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,43 +0,0 @@
|
|||
defmodule AshHq.Discord.Channel do
|
||||
@moduledoc """
|
||||
The channel is the discord forum channel. We explicitly configure which ones we import.
|
||||
"""
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
create :upsert do
|
||||
upsert? true
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
integer_primary_key :id, writable?: true, generated?: false
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :order, :integer do
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
has_many :threads, AshHq.Discord.Thread
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "discord_channels"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Discord
|
||||
define :read
|
||||
define :upsert
|
||||
end
|
||||
end
|
|
@ -1,96 +0,0 @@
|
|||
defmodule AshHq.Discord.Message do
|
||||
@moduledoc """
|
||||
Discord messages synchronized by the discord bot
|
||||
"""
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [
|
||||
AshHq.Docs.Extensions.RenderMarkdown,
|
||||
AshHq.Docs.Extensions.Search
|
||||
]
|
||||
|
||||
actions do
|
||||
defaults [:read, :destroy]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
argument :attachments, {:array, :map}
|
||||
argument :reactions, {:array, :map}
|
||||
change manage_relationship(:attachments, type: :direct_control)
|
||||
|
||||
change manage_relationship(:reactions,
|
||||
type: :direct_control,
|
||||
use_identities: [:unique_message_emoji]
|
||||
)
|
||||
end
|
||||
|
||||
update :update do
|
||||
primary? true
|
||||
argument :attachments, {:array, :map}
|
||||
argument :reactions, {:array, :map}
|
||||
change manage_relationship(:attachments, type: :direct_control)
|
||||
|
||||
change manage_relationship(:reactions,
|
||||
type: :direct_control,
|
||||
use_identities: [:unique_message_emoji]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes content: :content_html
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :content
|
||||
|
||||
type "Forum"
|
||||
|
||||
load_for_search [
|
||||
:channel_name,
|
||||
:thread_name
|
||||
]
|
||||
|
||||
has_name_attribute? false
|
||||
weight_content(-0.7)
|
||||
end
|
||||
|
||||
attributes do
|
||||
integer_primary_key :id, generated?: false, writable?: true
|
||||
|
||||
attribute :author, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :content, :string
|
||||
attribute :content_html, :string
|
||||
|
||||
attribute :timestamp, :utc_datetime do
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :thread, AshHq.Discord.Thread do
|
||||
attribute_type :integer
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
has_many :attachments, AshHq.Discord.Attachment
|
||||
has_many :reactions, AshHq.Discord.Reaction
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "discord_messages"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :thread, on_delete: :delete, on_update: :update
|
||||
end
|
||||
end
|
||||
|
||||
aggregates do
|
||||
first :channel_name, [:thread, :channel], :name
|
||||
first :thread_name, [:thread], :name
|
||||
end
|
||||
end
|
|
@ -1,43 +0,0 @@
|
|||
defmodule AshHq.Discord.Reaction do
|
||||
@moduledoc """
|
||||
Reactions store emoji reaction counts.
|
||||
"""
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :count, :integer do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :emoji, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :message, AshHq.Discord.Message do
|
||||
attribute_type :integer
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "discord_reactions"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :message, on_delete: :delete, on_update: :update
|
||||
end
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_message_emoji, [:emoji, :message_id]
|
||||
end
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
defmodule AshHq.Discord.Tag do
|
||||
@moduledoc "A tag that can be applied to a post. Currently uses CSV data layer and therefore is static"
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
create :upsert do
|
||||
upsert? true
|
||||
upsert_identity :unique_name_per_channel
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
integer_primary_key :id, generated?: false, writable?: true
|
||||
|
||||
attribute :name, :ci_string do
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :channel, AshHq.Discord.Channel do
|
||||
attribute_type :integer
|
||||
attribute_writable? true
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "discord_tags"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Discord
|
||||
define :upsert, args: [:channel_id, :id, :name]
|
||||
define :read
|
||||
define :destroy
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_name_per_channel, [:name, :channel_id]
|
||||
end
|
||||
end
|
|
@ -1,109 +0,0 @@
|
|||
defmodule AshHq.Discord.Thread do
|
||||
@moduledoc """
|
||||
A thread is an individual forum post (because they are really just fancy threads)
|
||||
"""
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
read :feed do
|
||||
pagination do
|
||||
countable true
|
||||
offset? true
|
||||
default_limit 25
|
||||
end
|
||||
|
||||
argument :channel, :integer do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
argument :tag_name, :string
|
||||
|
||||
prepare build(sort: [create_timestamp: :desc])
|
||||
|
||||
filter expr(
|
||||
channel_id == ^arg(:channel) and
|
||||
(is_nil(^arg(:tag_name)) or tags.name == ^arg(:tag_name))
|
||||
)
|
||||
end
|
||||
|
||||
create :upsert do
|
||||
upsert? true
|
||||
argument :messages, {:array, :map}
|
||||
argument :tags, {:array, :integer}
|
||||
|
||||
change manage_relationship(:messages, type: :direct_control)
|
||||
|
||||
change fn changeset, _ ->
|
||||
Ash.Changeset.after_action(changeset, fn changeset, thread ->
|
||||
tags = Ash.Changeset.get_argument(changeset, :tags) || []
|
||||
|
||||
# Not optimized in `manage_relationship`
|
||||
# bulk actions should make this unnecessary
|
||||
to_delete =
|
||||
from thread_tag in AshHq.Discord.ThreadTag,
|
||||
where: thread_tag.thread_id == ^thread.id,
|
||||
where: thread_tag.tag_id not in ^tags
|
||||
|
||||
AshHq.Repo.delete_all(to_delete)
|
||||
|
||||
Enum.map(tags, fn tag ->
|
||||
AshHq.Discord.ThreadTag.tag!(thread.id, tag)
|
||||
end)
|
||||
|
||||
{:ok, thread}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
integer_primary_key :id, generated?: false, writable?: true
|
||||
attribute :type, :integer
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :author, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :create_timestamp, :utc_datetime do
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
has_many :messages, AshHq.Discord.Message
|
||||
|
||||
belongs_to :channel, AshHq.Discord.Channel do
|
||||
attribute_type :integer
|
||||
allow_nil? false
|
||||
attribute_writable? true
|
||||
end
|
||||
|
||||
many_to_many :tags, AshHq.Discord.Tag do
|
||||
through AshHq.Discord.ThreadTag
|
||||
source_attribute_on_join_resource :thread_id
|
||||
destination_attribute_on_join_resource :tag_id
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "discord_threads"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Discord
|
||||
define :upsert
|
||||
define :by_id, action: :read, get_by: [:id]
|
||||
define :feed, args: [:channel]
|
||||
end
|
||||
end
|
|
@ -1,39 +0,0 @@
|
|||
defmodule AshHq.Discord.ThreadTag do
|
||||
@moduledoc "Joins a thread to a tag"
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
actions do
|
||||
defaults [:read, :destroy]
|
||||
|
||||
create :tag do
|
||||
upsert? true
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :thread, AshHq.Discord.Thread do
|
||||
primary_key? true
|
||||
allow_nil? false
|
||||
attribute_writable? true
|
||||
attribute_type :integer
|
||||
end
|
||||
|
||||
belongs_to :tag, AshHq.Discord.Tag do
|
||||
primary_key? true
|
||||
allow_nil? false
|
||||
attribute_writable? true
|
||||
attribute_type :integer
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "discord_thread_tags"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Discord
|
||||
define :tag, args: [:thread_id, :tag_id]
|
||||
end
|
||||
end
|
1
lib/ash_hq/docs/extensions/search/index.ex
Normal file
1
lib/ash_hq/docs/extensions/search/index.ex
Normal file
|
@ -0,0 +1 @@
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
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
|
||||
query_string = Ash.Query.get_argument(query, :query)
|
||||
to_load = AshHq.Docs.Extensions.Search.load_for_search(query.resource)
|
||||
|
||||
query.resource
|
||||
|> AshHq.Docs.Extensions.RenderMarkdown.render_attributes()
|
||||
|> Enum.reduce(query, fn {source, target}, query ->
|
||||
Ash.Query.deselect(query, [source, target])
|
||||
end)
|
||||
|> Ash.Query.load(search_headline: [query: query_string])
|
||||
|> Ash.Query.load(match_rank: [query: query_string])
|
||||
|> Ash.Query.load(to_load)
|
||||
|> Ash.Query.sort(match_rank: {:asc, %{query: query_string}})
|
||||
end
|
||||
end
|
|
@ -60,11 +60,6 @@ defmodule AshHq.Docs.Extensions.Search do
|
|||
type: :atom,
|
||||
doc: "The text field to be used in the search"
|
||||
],
|
||||
library_version_attribute: [
|
||||
type: :atom,
|
||||
default: :library_version_id,
|
||||
doc: "The attribute to use to filter by library version"
|
||||
],
|
||||
load_for_search: [
|
||||
type: {:list, :any},
|
||||
default: [],
|
||||
|
@ -140,10 +135,6 @@ defmodule AshHq.Docs.Extensions.Search do
|
|||
|> List.wrap()
|
||||
end
|
||||
|
||||
def library_version_attribute(resource) do
|
||||
Extension.get_opt(resource, [:search], :library_version_attribute, :library_version_id)
|
||||
end
|
||||
|
||||
def load_for_search(resource) do
|
||||
Extension.get_opt(resource, [:search], :load_for_search, :library_version_id)
|
||||
end
|
||||
|
|
|
@ -4,15 +4,8 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
|
|||
|
||||
* 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 `matches` calculation
|
||||
* Adds relevant indexes using custom sql statements
|
||||
* Adds a `match_rank` calculation.
|
||||
* Adds a search action
|
||||
* Adds a code interface for the search action
|
||||
"""
|
||||
use Spark.Dsl.Transformer
|
||||
import Ash.Filter.TemplateHelpers
|
||||
require Ash.Query
|
||||
alias Spark.Dsl.Transformer
|
||||
|
||||
|
@ -26,36 +19,10 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
|
|||
|
||||
config = %{
|
||||
name_attribute: name_attribute,
|
||||
doc_attribute: Transformer.get_option(dsl_state, [:search], :doc_attribute),
|
||||
library_version_attribute:
|
||||
Transformer.get_option(dsl_state, [:search], :library_version_attribute) ||
|
||||
:library_version_id,
|
||||
table: Transformer.get_option(dsl_state, [:postgres], :table),
|
||||
sanitized_name_attribute: sanitized_name_attribute
|
||||
}
|
||||
|
||||
currently_ignored_attributes =
|
||||
AshPostgres.DataLayer.Info.migration_ignore_attributes(dsl_state)
|
||||
|
||||
dsl_state
|
||||
|> add_sanitized_name(config)
|
||||
|> add_search_action(config)
|
||||
|> add_code_interface()
|
||||
|> Transformer.set_option([:postgres], :migration_ignore_attributes, [
|
||||
:searchable | currently_ignored_attributes
|
||||
])
|
||||
|> add_search_headline_calculation(config)
|
||||
|> add_matches_calculation()
|
||||
|> add_full_text_column(config)
|
||||
|> add_full_text_index()
|
||||
|> add_match_rank_calculation(config)
|
||||
|> Ash.Resource.Builder.add_preparation(
|
||||
{AshHq.Docs.Extensions.Search.Preparations.DeselectSearchable, []}
|
||||
)
|
||||
|> Ash.Resource.Builder.add_attribute(:searchable, AshHq.Docs.Search.Types.TsVector,
|
||||
generated?: true,
|
||||
private?: true
|
||||
)
|
||||
{:ok, add_sanitized_name(dsl_state, config)}
|
||||
end
|
||||
|
||||
defp add_sanitized_name(dsl_state, config) do
|
||||
|
@ -104,253 +71,6 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
|
|||
end
|
||||
end
|
||||
|
||||
defp add_full_text_index(dsl_state) do
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:postgres, :custom_indexes],
|
||||
Transformer.build_entity!(
|
||||
AshPostgres.DataLayer,
|
||||
[:postgres, :custom_indexes],
|
||||
:index,
|
||||
fields: [:searchable],
|
||||
using: "GIN"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp add_full_text_column(dsl_state, config) do
|
||||
if config.doc_attribute do
|
||||
if Transformer.get_option(dsl_state, [:search], :has_name_attribute?, true) do
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:postgres, :custom_statements],
|
||||
Transformer.build_entity!(
|
||||
AshPostgres.DataLayer,
|
||||
[:postgres, :custom_statements],
|
||||
:statement,
|
||||
name: :search_column,
|
||||
up: """
|
||||
ALTER TABLE #{config.table}
|
||||
ADD COLUMN searchable tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', #{config.name_attribute}), 'A') ||
|
||||
setweight(to_tsvector('english', #{config.doc_attribute}), 'D')
|
||||
) STORED;
|
||||
""",
|
||||
down: """
|
||||
ALTER TABLE #{config.table}
|
||||
DROP COLUMN searchable
|
||||
"""
|
||||
)
|
||||
)
|
||||
else
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:postgres, :custom_statements],
|
||||
Transformer.build_entity!(
|
||||
AshPostgres.DataLayer,
|
||||
[:postgres, :custom_statements],
|
||||
:statement,
|
||||
name: :search_column,
|
||||
up: """
|
||||
ALTER TABLE #{config.table}
|
||||
ADD COLUMN searchable tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', #{config.doc_attribute}), 'D')
|
||||
) STORED;
|
||||
""",
|
||||
down: """
|
||||
ALTER TABLE #{config.table}
|
||||
DROP COLUMN searchable
|
||||
"""
|
||||
)
|
||||
)
|
||||
end
|
||||
else
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:postgres, :custom_statements],
|
||||
Transformer.build_entity!(
|
||||
AshPostgres.DataLayer,
|
||||
[:postgres, :custom_statements],
|
||||
:statement,
|
||||
name: :search_column,
|
||||
up: """
|
||||
ALTER TABLE #{config.table}
|
||||
ADD COLUMN searchable tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', #{config.name_attribute}), 'A')
|
||||
) STORED;
|
||||
""",
|
||||
down: """
|
||||
ALTER TABLE #{config.table}
|
||||
DROP COLUMN searchable
|
||||
"""
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_match_rank_calculation(dsl_state, _config) do
|
||||
weight_content = AshHq.Docs.Extensions.Search.weight_content(dsl_state)
|
||||
|
||||
dsl_state
|
||||
|> Transformer.add_entity(
|
||||
[:calculations],
|
||||
Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
|
||||
name: :match_rank,
|
||||
type: :float,
|
||||
private?: true,
|
||||
arguments: [query_argument()],
|
||||
calculation:
|
||||
Ash.Query.expr(
|
||||
fragment(
|
||||
"(ts_rank_cd('{0.05, 0.1, 0.1, 1.0}', ?, websearch_to_tsquery(?), 32) + ?)",
|
||||
^ref(:searchable),
|
||||
^arg(:query),
|
||||
^weight_content
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp add_matches_calculation(dsl_state) do
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:calculations],
|
||||
Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
|
||||
name: :matches,
|
||||
type: :boolean,
|
||||
private?: true,
|
||||
arguments: [query_argument()],
|
||||
calculation:
|
||||
Ash.Query.expr(
|
||||
fragment(
|
||||
"(? @@ websearch_to_tsquery(?))",
|
||||
^ref(:searchable),
|
||||
^arg(:query)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# defp add_name_matches_calculation(dsl_state, config) do
|
||||
# if AshHq.Docs.Extensions.Search.has_name_attribute?(dsl_state) do
|
||||
# Transformer.add_entity(
|
||||
# dsl_state,
|
||||
# [:calculations],
|
||||
# Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
|
||||
# name: :name_matches,
|
||||
# type: :boolean,
|
||||
# arguments: [query_argument(), similarity_argument()],
|
||||
# private?: true,
|
||||
# calculation:
|
||||
# Ash.Query.expr(
|
||||
# contains(
|
||||
# fragment("lower(?)", ^ref(config.name_attribute)),
|
||||
# fragment("lower(?)", ^arg(:query))
|
||||
# )
|
||||
# )
|
||||
# )
|
||||
# )
|
||||
# else
|
||||
# dsl_state
|
||||
# end
|
||||
# end
|
||||
|
||||
defp add_search_headline_calculation(dsl_state, config) do
|
||||
if config.doc_attribute do
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:calculations],
|
||||
Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
|
||||
name: :search_headline,
|
||||
type: :string,
|
||||
private?: true,
|
||||
arguments: [query_argument()],
|
||||
calculation:
|
||||
Ash.Query.expr(
|
||||
# credo:disable-for-next-line
|
||||
fragment(
|
||||
"ts_headline('english', ?, websearch_to_tsquery('english', ?), 'MaxFragments=2,StartSel=\"<span class=\"\"search-hit\"\">\", StopSel=</span>')",
|
||||
^ref(config.doc_attribute),
|
||||
^arg(:query)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
else
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:calculations],
|
||||
Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
|
||||
name: :search_headline,
|
||||
type: :string,
|
||||
arguments: [query_argument()],
|
||||
private?: true,
|
||||
calculation: Ash.Query.expr("")
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp query_argument do
|
||||
Transformer.build_entity!(
|
||||
Ash.Resource.Dsl,
|
||||
[:calculations, :calculate],
|
||||
:argument,
|
||||
type: :string,
|
||||
name: :query,
|
||||
allow_nil?: false
|
||||
)
|
||||
end
|
||||
|
||||
defp add_search_action(dsl_state, _config) do
|
||||
query_argument =
|
||||
Transformer.build_entity!(
|
||||
Ash.Resource.Dsl,
|
||||
[:actions, :read],
|
||||
:argument,
|
||||
type: :string,
|
||||
name: :query
|
||||
)
|
||||
|
||||
{arguments, filter} =
|
||||
{[query_argument], Ash.Query.expr(matches(query: arg(:query)))}
|
||||
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:actions],
|
||||
Transformer.build_entity!(Ash.Resource.Dsl, [:actions], :read,
|
||||
name: :search,
|
||||
arguments: arguments,
|
||||
preparations: search_preparations(),
|
||||
filter: filter
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp add_code_interface(dsl_state) do
|
||||
Transformer.add_entity(
|
||||
dsl_state,
|
||||
[:code_interface],
|
||||
Transformer.build_entity!(Ash.Resource.Dsl, [:code_interface], :define,
|
||||
name: :search,
|
||||
args: [:query]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp search_preparations do
|
||||
[
|
||||
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :read], :prepare,
|
||||
preparation: AshHq.Extensions.Search.Preparations.LoadSearchData
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
def before?(Ash.Resource.Transformers.SetTypes), do: true
|
||||
def before?(_), do: false
|
||||
def after?(Ash.Resource.Transformers.SetPrimaryActions), do: true
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule AshHq.Docs.Extensions.Search.Types do
|
|||
A static list of all search types that currently exist
|
||||
"""
|
||||
|
||||
@search_types [AshHq.Docs.Registry, AshHq.Discord.Registry]
|
||||
@search_types [AshHq.Docs.Registry]
|
||||
|> Enum.flat_map(&Ash.Registry.Info.entries/1)
|
||||
|> Enum.filter(&(AshHq.Docs.Extensions.Search in Spark.extensions(&1)))
|
||||
|> Enum.map(&AshHq.Docs.Extensions.Search.type/1)
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
defmodule AshHq.Docs.Search.Types.TsVector do
|
||||
@moduledoc "A stub for a tsvector type that should never actually get loaded."
|
||||
use Ash.Type
|
||||
|
||||
def storage_type, do: :tsvector
|
||||
def cast_in_query?(_), do: false
|
||||
|
||||
defdelegate cast_input(value, constraints), to: Ash.Type.String
|
||||
defdelegate cast_stored(value, constraints), to: Ash.Type.String
|
||||
defdelegate dump_to_native(value, constraints), to: Ash.Type.String
|
||||
end
|
|
@ -135,8 +135,6 @@ defmodule AshHq.Docs.Importer do
|
|||
Logger.error(
|
||||
"Failed to import version #{name} #{version} #{Exception.format(:error, e, __STACKTRACE__)}"
|
||||
)
|
||||
|
||||
e
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
@ -219,34 +217,21 @@ defmodule AshHq.Docs.Importer do
|
|||
end
|
||||
|
||||
if result do
|
||||
{:ok, library_version} =
|
||||
AshHq.Repo.transaction(fn ->
|
||||
Logger.info("Starting import of #{name}: #{version}")
|
||||
Logger.info("Starting import of #{name}: #{version}")
|
||||
|
||||
id =
|
||||
case LibraryVersion.by_version(library.id, version) do
|
||||
{:ok, version} ->
|
||||
LibraryVersion.destroy!(version)
|
||||
version.id
|
||||
|
||||
_ ->
|
||||
Ash.UUID.generate()
|
||||
end
|
||||
|
||||
LibraryVersion.build!(
|
||||
library.id,
|
||||
version,
|
||||
%{
|
||||
timeout: :infinity,
|
||||
id: id,
|
||||
extensions: result[:extensions],
|
||||
doc: result[:doc],
|
||||
guides: result[:guides],
|
||||
modules: result[:modules],
|
||||
mix_tasks: result[:mix_tasks]
|
||||
}
|
||||
)
|
||||
end)
|
||||
library_version =
|
||||
LibraryVersion.build!(
|
||||
library.id,
|
||||
version,
|
||||
%{
|
||||
timeout: :infinity,
|
||||
extensions: result[:extensions],
|
||||
doc: result[:doc],
|
||||
guides: result[:guides],
|
||||
modules: result[:modules],
|
||||
mix_tasks: result[:mix_tasks]
|
||||
}
|
||||
)
|
||||
|
||||
LibraryVersion
|
||||
|> Ash.Query.for_read(:read)
|
||||
|
|
214
lib/ash_hq/docs/indexer.ex
Normal file
214
lib/ash_hq/docs/indexer.ex
Normal file
|
@ -0,0 +1,214 @@
|
|||
defmodule AshHq.Docs.Indexer do
|
||||
use GenServer
|
||||
|
||||
require Ash.Query
|
||||
|
||||
def start_link(state, opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, state, opts)
|
||||
end
|
||||
|
||||
def init(_) do
|
||||
{:ok, %{haystack: haystack()}, {:continue, :index}}
|
||||
end
|
||||
|
||||
def haystack do
|
||||
Haystack.index(Haystack.new(), :search, fn index ->
|
||||
index
|
||||
|> Haystack.Index.ref(Haystack.Index.Field.term("id"))
|
||||
|> Haystack.Index.field(Haystack.Index.Field.new("name"))
|
||||
|> Haystack.Index.field(Haystack.Index.Field.new("call_name"))
|
||||
|> Haystack.Index.field(Haystack.Index.Field.new("doc"))
|
||||
|> Haystack.Index.field(Haystack.Index.Field.new("library_name"))
|
||||
|> Haystack.Index.storage(storage())
|
||||
end)
|
||||
end
|
||||
|
||||
def storage do
|
||||
Haystack.Storage.ETS.new(name: :search, table: :search)
|
||||
end
|
||||
|
||||
def search(search) do
|
||||
tokens = Haystack.Tokenizer.tokenize(search)
|
||||
tokens = Haystack.Transformer.pipeline(tokens, Haystack.Transformer.default())
|
||||
|
||||
Haystack.index(haystack(), :search, fn index ->
|
||||
query =
|
||||
Enum.reduce(tokens, Haystack.Query.Clause.new(:any), fn token, clause ->
|
||||
Enum.reduce(
|
||||
Map.values(Map.take(index.fields, ["name", "call_name", "description"])),
|
||||
clause,
|
||||
fn field, clause ->
|
||||
Haystack.Query.Clause.expressions(clause, [
|
||||
Haystack.Query.Expression.new(:match, field: field.k, term: token.v)
|
||||
])
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
||||
Haystack.Query.new()
|
||||
|> Haystack.Query.clause(query)
|
||||
|> Haystack.Query.run(index)
|
||||
end)
|
||||
|> Stream.map(fn item ->
|
||||
[type, id] = String.split(item.ref, "|")
|
||||
%{id: id, type: type, score: item.score}
|
||||
end)
|
||||
|> Enum.group_by(& &1.type)
|
||||
|> Enum.flat_map(fn {type, items} ->
|
||||
resource =
|
||||
case type do
|
||||
"dsl" -> AshHq.Docs.Dsl
|
||||
"guide" -> AshHq.Docs.Guide
|
||||
"option" -> AshHq.Docs.Option
|
||||
"module" -> AshHq.Docs.Module
|
||||
"mix_task" -> AshHq.Docs.MixTask
|
||||
"function" -> AshHq.Docs.Function
|
||||
end
|
||||
|
||||
ids = Enum.map(items, & &1.id)
|
||||
|
||||
scores = Map.new(items, &{&1.id, &1.score})
|
||||
|
||||
resource
|
||||
|> Ash.Query.filter(id in ^ids)
|
||||
|> Ash.Query.load(AshHq.Docs.Extensions.Search.load_for_search(resource))
|
||||
|> AshHq.Docs.read!()
|
||||
|> Enum.map(fn item ->
|
||||
Ash.Resource.put_metadata(item, :search_score, scores[item.id])
|
||||
end)
|
||||
end)
|
||||
|> Enum.sort_by(&{!exact_match?(&1, search), -&1.__metadata__.search_score})
|
||||
end
|
||||
|
||||
defp exact_match?(record, search) do
|
||||
record
|
||||
|> Map.take([:name, :call_name])
|
||||
|> Map.values()
|
||||
|> Enum.any?(fn value ->
|
||||
is_binary(value) &&
|
||||
String.downcase(value) == String.downcase(String.trim_trailing(search, "("))
|
||||
end)
|
||||
end
|
||||
|
||||
def handle_continue(:index, state) do
|
||||
{:noreply, index(state)}
|
||||
end
|
||||
|
||||
def handle_info(:index, state) do
|
||||
{:noreply, index(state)}
|
||||
end
|
||||
|
||||
def handle_info(_, state), do: {:noreply, state}
|
||||
|
||||
defp index(state) do
|
||||
haystack =
|
||||
Haystack.index(state.haystack, :search, fn index ->
|
||||
[
|
||||
dsls(),
|
||||
guides(),
|
||||
options(),
|
||||
modules(),
|
||||
mix_tasks(),
|
||||
functions()
|
||||
]
|
||||
|> Stream.concat()
|
||||
|> Stream.chunk_every(100)
|
||||
|> Enum.each(&Haystack.Index.add(index, &1))
|
||||
end)
|
||||
|
||||
%{state | haystack: haystack}
|
||||
after
|
||||
Process.send_after(self(), :index, :timer.hours(6))
|
||||
end
|
||||
|
||||
defp dsls do
|
||||
AshHq.Docs.Dsl
|
||||
|> Ash.Query.load([:library_name, :extension_module])
|
||||
|> AshHq.Docs.stream!()
|
||||
|> Stream.map(fn dsl ->
|
||||
%{
|
||||
"id" => id("dsl", dsl.id),
|
||||
"name" => dsl.name,
|
||||
"library_name" => dsl.library_name,
|
||||
"doc" => dsl.doc,
|
||||
"call_name" => "#{dsl.extension_module}.#{dsl.sanitized_path}.#{dsl.name}"
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp guides do
|
||||
AshHq.Docs.Guide
|
||||
|> Ash.Query.load(library_version: :library)
|
||||
|> AshHq.Docs.stream!()
|
||||
|> Stream.map(fn guide ->
|
||||
%{
|
||||
"id" => id("guide", guide.id),
|
||||
"name" => guide.name,
|
||||
"library_name" => guide.library_version.library.name,
|
||||
"doc" => guide.text
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp options do
|
||||
AshHq.Docs.Option
|
||||
|> Ash.Query.load([:library_name, :extension_module])
|
||||
|> AshHq.Docs.stream!()
|
||||
|> Stream.map(fn option ->
|
||||
%{
|
||||
"id" => id("option", option.id),
|
||||
"name" => option.name,
|
||||
"library_name" => option.library_name,
|
||||
"doc" => option.doc,
|
||||
"call_name" => "#{option.extension_module}.#{option.sanitized_path}.#{option.name}"
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp modules do
|
||||
AshHq.Docs.Module
|
||||
|> Ash.Query.load([:library_name])
|
||||
|> AshHq.Docs.stream!()
|
||||
|> Stream.map(fn module ->
|
||||
%{
|
||||
"id" => id("module", module.id),
|
||||
"name" => module.name,
|
||||
"library_name" => module.library_name,
|
||||
"doc" => module.doc,
|
||||
"call_name" => module.name
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp mix_tasks do
|
||||
AshHq.Docs.MixTask
|
||||
|> Ash.Query.load([:library_name])
|
||||
|> AshHq.Docs.stream!()
|
||||
|> Stream.map(fn mix_task ->
|
||||
%{
|
||||
"id" => id("mix_task", mix_task.id),
|
||||
"name" => mix_task.module_name,
|
||||
"library_name" => mix_task.library_name,
|
||||
"doc" => mix_task.doc,
|
||||
"call_name" => mix_task.name
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp functions do
|
||||
AshHq.Docs.Function
|
||||
|> Ash.Query.load([:library_name, :call_name])
|
||||
|> AshHq.Docs.stream!()
|
||||
|> Stream.map(fn function ->
|
||||
%{
|
||||
"id" => id("function", function.id),
|
||||
"name" => function.name,
|
||||
"library_name" => function.library_name,
|
||||
"doc" => function.doc,
|
||||
"call_name" => function.call_name
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp id(type, id), do: "#{type}|#{id}"
|
||||
end
|
|
@ -2,9 +2,41 @@ defmodule AshHq.Docs.Dsl do
|
|||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
data_layer: AshSqlite.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
sqlite do
|
||||
table "dsls"
|
||||
repo AshHq.SqliteRepo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
reference :dsl, on_delete: :delete
|
||||
end
|
||||
|
||||
migration_defaults optional_args: "[]"
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:extension_name,
|
||||
:extension_target,
|
||||
:extension_module,
|
||||
:library_name
|
||||
]
|
||||
|
||||
weight_content(0.2)
|
||||
|
||||
sanitized_name_attribute :sanitized_path
|
||||
use_path_for_name? true
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
@ -35,26 +67,6 @@ defmodule AshHq.Docs.Dsl do
|
|||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:extension_name,
|
||||
:extension_target,
|
||||
:extension_module,
|
||||
:library_name
|
||||
]
|
||||
|
||||
weight_content(0.2)
|
||||
|
||||
sanitized_name_attribute :sanitized_path
|
||||
use_path_for_name? true
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -113,18 +125,6 @@ defmodule AshHq.Docs.Dsl do
|
|||
has_many :dsls, __MODULE__
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "dsls"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
reference :dsl, on_delete: :delete
|
||||
end
|
||||
|
||||
migration_defaults optional_args: "[]"
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
define :read
|
||||
|
@ -134,14 +134,14 @@ defmodule AshHq.Docs.Dsl do
|
|||
description "An entity or section in an Ash DSL"
|
||||
end
|
||||
|
||||
aggregates do
|
||||
first :extension_type, :extension, :type
|
||||
first :extension_order, :extension, :order
|
||||
first :extension_name, :extension, :name
|
||||
first :extension_module, :extension, :module
|
||||
first :extension_target, :extension, :target
|
||||
first :version_name, :library_version, :version
|
||||
first :library_name, [:library_version, :library], :name
|
||||
first :library_id, [:library_version, :library], :id
|
||||
calculations do
|
||||
calculate :extension_type, :string, expr(extension.type)
|
||||
calculate :extension_order, :integer, expr(extension.order)
|
||||
calculate :extension_name, :string, expr(extension.name)
|
||||
calculate :extension_module, :string, expr(extension.module)
|
||||
calculate :extension_target, :string, expr(extension.target)
|
||||
calculate :version_name, :string, expr(library_version.version)
|
||||
calculate :library_name, :string, expr(library_version.library.name)
|
||||
calculate :library_id, :string, expr(library_version.library.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,9 +2,27 @@ defmodule AshHq.Docs.Extension do
|
|||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
data_layer: AshSqlite.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
sqlite do
|
||||
table "extensions"
|
||||
repo AshHq.SqliteRepo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
load_for_search library_version: [:library_display_name, :library_name]
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
@ -31,15 +49,6 @@ defmodule AshHq.Docs.Extension do
|
|||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
load_for_search library_version: [:library_display_name, :library_name]
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -86,15 +95,6 @@ defmodule AshHq.Docs.Extension do
|
|||
has_many :options, AshHq.Docs.Option
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "extensions"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
|
||||
|
|
|
@ -2,9 +2,39 @@ defmodule AshHq.Docs.Function do
|
|||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
data_layer: AshSqlite.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
sqlite do
|
||||
table "functions"
|
||||
repo AshHq.SqliteRepo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:module_name,
|
||||
:call_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
type "Code"
|
||||
|
||||
show_docs_on :module_sanitized_name
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html, heads: :heads_html
|
||||
header_ids? false
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
@ -26,26 +56,6 @@ defmodule AshHq.Docs.Function do
|
|||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:module_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
type "Code"
|
||||
|
||||
show_docs_on :module_sanitized_name
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html, heads: :heads_html
|
||||
header_ids? false
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -105,15 +115,6 @@ defmodule AshHq.Docs.Function do
|
|||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "functions"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
end
|
||||
|
@ -122,11 +123,12 @@ defmodule AshHq.Docs.Function do
|
|||
description "A function in a module exposed by an Ash library"
|
||||
end
|
||||
|
||||
aggregates do
|
||||
first :version_name, :library_version, :version
|
||||
first :library_name, [:library_version, :library], :name
|
||||
first :library_id, [:library_version, :library], :id
|
||||
first :module_name, :module, :name
|
||||
first :module_sanitized_name, :module, :sanitized_name
|
||||
calculations do
|
||||
calculate :version_name, :string, expr(library_version.version)
|
||||
calculate :library_name, :string, expr(library_version.library.name)
|
||||
calculate :library_id, :uuid, expr(library_version.library.id)
|
||||
calculate :module_name, :string, expr(module.name)
|
||||
calculate :module_sanitized_name, :string, expr(module.sanitized_name)
|
||||
calculate :call_name, :string, expr(module_name <> "." <> name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule AshHq.Docs.Guide do
|
||||
@moduledoc false
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
data_layer: AshSqlite.DataLayer,
|
||||
extensions: [
|
||||
AshHq.Docs.Extensions.Search,
|
||||
AshHq.Docs.Extensions.RenderMarkdown,
|
||||
|
@ -9,6 +9,43 @@ defmodule AshHq.Docs.Guide do
|
|||
AshAdmin.Resource
|
||||
]
|
||||
|
||||
sqlite do
|
||||
repo AshHq.SqliteRepo
|
||||
table "guides"
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :text
|
||||
show_docs_on [:sanitized_name, :sanitized_route]
|
||||
type "Guides"
|
||||
load_for_search [:library_name, library_version: [:library_name, :library_display_name]]
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes text: :text_html
|
||||
table_of_contents? true
|
||||
end
|
||||
|
||||
graphql do
|
||||
type :guide
|
||||
|
||||
queries do
|
||||
list :list_guides, :read_for_version
|
||||
end
|
||||
end
|
||||
|
||||
admin do
|
||||
form do
|
||||
field :text do
|
||||
type :markdown
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :update, :destroy]
|
||||
|
||||
|
@ -34,34 +71,6 @@ defmodule AshHq.Docs.Guide do
|
|||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :text
|
||||
show_docs_on [:sanitized_name, :sanitized_route]
|
||||
type "Guides"
|
||||
load_for_search library_version: [:library_name, :library_display_name]
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes text: :text_html
|
||||
table_of_contents? true
|
||||
end
|
||||
|
||||
graphql do
|
||||
type :guide
|
||||
|
||||
queries do
|
||||
list :list_guides, :read_for_version
|
||||
end
|
||||
end
|
||||
|
||||
admin do
|
||||
form do
|
||||
field :text do
|
||||
type :markdown
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -112,15 +121,6 @@ defmodule AshHq.Docs.Guide do
|
|||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
repo AshHq.Repo
|
||||
table "guides"
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
end
|
||||
|
@ -146,4 +146,8 @@ defmodule AshHq.Docs.Guide do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate :library_name, :string, expr(library_version.library.name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,8 +25,6 @@ defmodule AshHq.Docs.Library.Agent do
|
|||
end
|
||||
|
||||
def clear do
|
||||
AshHq.Discord.Listener.rebuild()
|
||||
|
||||
Agent.update(__MODULE__, fn _state ->
|
||||
nil
|
||||
end)
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
defmodule AshHq.Docs.Library do
|
||||
@moduledoc false
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
data_layer: AshSqlite.DataLayer
|
||||
|
||||
sqlite do
|
||||
table "libraries"
|
||||
repo AshHq.SqliteRepo
|
||||
|
||||
migration_defaults module_prefixes: "[]", skip_versions: "[]"
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :update, :destroy]
|
||||
|
@ -67,16 +74,10 @@ defmodule AshHq.Docs.Library do
|
|||
|
||||
has_one :latest_library_version, AshHq.Docs.LibraryVersion do
|
||||
sort version: :desc
|
||||
from_many? true
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "libraries"
|
||||
repo AshHq.Repo
|
||||
|
||||
migration_defaults module_prefixes: "[]", skip_versions: "[]"
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
|
||||
|
@ -103,13 +104,8 @@ defmodule AshHq.Docs.Library do
|
|||
end
|
||||
end
|
||||
|
||||
aggregates do
|
||||
first :latest_version, :versions, :version do
|
||||
sort version: :desc
|
||||
end
|
||||
|
||||
first :latest_version_id, :versions, :id do
|
||||
sort version: :desc
|
||||
end
|
||||
calculations do
|
||||
calculate :latest_version, :string, expr(latest_library_version.version)
|
||||
calculate :latest_version_id, :uuid, expr(latest_library_version.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,9 +2,19 @@ defmodule AshHq.Docs.LibraryVersion do
|
|||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
data_layer: AshSqlite.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
sqlite do
|
||||
table "library_versions"
|
||||
repo AshHq.SqliteRepo
|
||||
end
|
||||
|
||||
search do
|
||||
name_attribute :version
|
||||
load_for_search [:library_name, :library_display_name]
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
@ -81,12 +91,6 @@ defmodule AshHq.Docs.LibraryVersion do
|
|||
end
|
||||
end
|
||||
|
||||
search do
|
||||
name_attribute :version
|
||||
library_version_attribute :id
|
||||
load_for_search [:library_name, :library_display_name]
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -113,11 +117,6 @@ defmodule AshHq.Docs.LibraryVersion do
|
|||
has_many :mix_tasks, AshHq.Docs.MixTask
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "library_versions"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
define :build, args: [:library, :version]
|
||||
|
@ -143,18 +142,8 @@ defmodule AshHq.Docs.LibraryVersion do
|
|||
end
|
||||
end
|
||||
|
||||
preparations do
|
||||
prepare AshHq.Docs.LibraryVersion.Preparations.SortBySortableVersionInstead
|
||||
end
|
||||
|
||||
aggregates do
|
||||
first :library_name, :library, :name
|
||||
first :library_display_name, :library, :display_name
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate :sortable_version,
|
||||
{:array, :string},
|
||||
expr(fragment("string_to_array(?, '.')", version))
|
||||
calculate :library_name, :string, expr(library.name)
|
||||
calculate :library_display_name, :string, expr(library.display_name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
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
|
||||
%{query | sort: replace_sort(query.sort)}
|
||||
end
|
||||
|
||||
defp replace_sort(nil), do: nil
|
||||
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
|
|
@ -2,22 +2,15 @@ defmodule AshHq.Docs.MixTask do
|
|||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
data_layer: AshSqlite.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
sqlite do
|
||||
table "mix_tasks"
|
||||
repo AshHq.SqliteRepo
|
||||
|
||||
read :read do
|
||||
primary? true
|
||||
pagination offset?: true, countable: true, default_limit: 25, required?: false
|
||||
end
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
argument :library_version, :uuid
|
||||
|
||||
change manage_relationship(:library_version, type: :append_and_remove)
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -39,6 +32,27 @@ defmodule AshHq.Docs.MixTask do
|
|||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
read :read do
|
||||
primary? true
|
||||
|
||||
pagination keyset?: true,
|
||||
offset?: true,
|
||||
countable: true,
|
||||
default_limit: 25,
|
||||
required?: false
|
||||
end
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
argument :library_version, :uuid
|
||||
|
||||
change manage_relationship(:library_version, type: :append_and_remove)
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -79,15 +93,6 @@ defmodule AshHq.Docs.MixTask do
|
|||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "mix_tasks"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
end
|
||||
|
@ -96,9 +101,9 @@ defmodule AshHq.Docs.MixTask do
|
|||
description "Represents a mix task that has been exposed by a library"
|
||||
end
|
||||
|
||||
aggregates do
|
||||
first :version_name, :library_version, :version
|
||||
first :library_name, [:library_version, :library], :name
|
||||
first :library_id, [:library_version, :library], :id
|
||||
calculations do
|
||||
calculate :version_name, :string, expr(library_version.version)
|
||||
calculate :library_name, :string, expr(library_version.library.name)
|
||||
calculate :library_id, :uuid, expr(library_version.library.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,9 +2,36 @@ defmodule AshHq.Docs.Module do
|
|||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
data_layer: AshSqlite.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
sqlite do
|
||||
table "modules"
|
||||
repo AshHq.SqliteRepo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
weight_content(0.5)
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
type "Code"
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
@ -29,24 +56,6 @@ defmodule AshHq.Docs.Module do
|
|||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
weight_content(0.5)
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
type "Code"
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -87,15 +96,6 @@ defmodule AshHq.Docs.Module do
|
|||
has_many :functions, AshHq.Docs.Function
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "modules"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
end
|
||||
|
@ -104,9 +104,9 @@ defmodule AshHq.Docs.Module do
|
|||
description "Represents a module that has been exposed by a library"
|
||||
end
|
||||
|
||||
aggregates do
|
||||
first :version_name, :library_version, :version
|
||||
first :library_name, [:library_version, :library], :name
|
||||
first :library_id, [:library_version, :library], :id
|
||||
calculations do
|
||||
calculate :version_name, :string, expr(library_version.version)
|
||||
calculate :library_name, :string, expr(library_version.library.name)
|
||||
calculate :library_id, :uuid, expr(library_version.library.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,9 +2,39 @@ defmodule AshHq.Docs.Option do
|
|||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
data_layer: AshSqlite.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
sqlite do
|
||||
table "options"
|
||||
repo AshHq.SqliteRepo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
reference :dsl, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:extension_name,
|
||||
:extension_module,
|
||||
:extension_target,
|
||||
:library_name
|
||||
]
|
||||
|
||||
sanitized_name_attribute :sanitized_path
|
||||
use_path_for_name? true
|
||||
add_name_to_path? false
|
||||
show_docs_on :dsl_sanitized_path
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
@ -31,26 +61,6 @@ defmodule AshHq.Docs.Option do
|
|||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:extension_name,
|
||||
:extension_module,
|
||||
:extension_target,
|
||||
:library_name
|
||||
]
|
||||
|
||||
sanitized_name_attribute :sanitized_path
|
||||
use_path_for_name? true
|
||||
add_name_to_path? false
|
||||
show_docs_on :dsl_sanitized_path
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -105,16 +115,6 @@ defmodule AshHq.Docs.Option do
|
|||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "options"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
reference :dsl, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for AshHq.Docs
|
||||
define :read
|
||||
|
@ -124,15 +124,15 @@ defmodule AshHq.Docs.Option do
|
|||
description "Represents an option on a DSL section or entity"
|
||||
end
|
||||
|
||||
aggregates do
|
||||
first :extension_type, [:dsl, :extension], :type
|
||||
first :extension_name, [:dsl, :extension], :name
|
||||
first :extension_order, [:dsl, :extension], :order
|
||||
first :extension_target, [:dsl, :extension], :target
|
||||
first :extension_module, [:dsl, :extension], :module
|
||||
first :version_name, :library_version, :version
|
||||
first :library_name, [:library_version, :library], :name
|
||||
first :library_id, [:library_version, :library], :id
|
||||
first :dsl_sanitized_path, :dsl, :sanitized_path
|
||||
calculations do
|
||||
calculate :extension_type, :string, expr(dsl.extension.type)
|
||||
calculate :extension_name, :string, expr(dsl.extension.name)
|
||||
calculate :extension_order, :integer, expr(dsl.extension.order)
|
||||
calculate :extension_target, :string, expr(dsl.extension.target)
|
||||
calculate :extension_module, :string, expr(dsl.extension.module)
|
||||
calculate :version_name, :string, expr(library_version.version)
|
||||
calculate :library_name, :string, expr(library_version.library.name)
|
||||
calculate :library_id, :string, expr(library_version.library.id)
|
||||
calculate :dsl_sanitized_path, :string, expr(dsl.sanitized_path)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule AshHq.Release do
|
|||
@app :ash_hq
|
||||
def migrate do
|
||||
load_app()
|
||||
System.cmd("sqlite3", ["litefs/db", "VACUUM;"])
|
||||
|
||||
for repo <- repos() do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||
|
@ -23,14 +24,7 @@ defmodule AshHq.Release do
|
|||
end
|
||||
|
||||
defp repos do
|
||||
apis()
|
||||
|> Enum.flat_map(fn api ->
|
||||
api
|
||||
|> Ash.Api.Info.resources()
|
||||
|> Enum.filter(&(AshPostgres.DataLayer in Spark.extensions(&1)))
|
||||
|> Enum.map(&AshPostgres.DataLayer.Info.repo/1)
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
Application.fetch_env!(@app, :ecto_repos)
|
||||
end
|
||||
|
||||
defp apis do
|
||||
|
|
4
lib/ash_hq/sqlite_repo.ex
Normal file
4
lib/ash_hq/sqlite_repo.ex
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule AshHq.SqliteRepo do
|
||||
use AshSqlite.Repo,
|
||||
otp_app: :ash_hq
|
||||
end
|
|
@ -95,6 +95,42 @@ defmodule AshHqWeb.Components.Search do
|
|||
~F"""
|
||||
<div class="divide-y">
|
||||
{#for item <- items}
|
||||
{#if item.__struct__ == AshHq.Docs.Guide}
|
||||
<LivePatch
|
||||
class="block w-full text-left border-base-light-300 dark:border-base-dark-600"
|
||||
to={DocRoutes.doc_link(item)}
|
||||
opts={id: "result-#{item.id}", "phx-click": @close}
|
||||
>
|
||||
<div class={
|
||||
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-1 w-full",
|
||||
"bg-base-light-200 dark:bg-base-dark-700": @selected_item.id == item.id
|
||||
}>
|
||||
<div class="flex justify-start items-center space-x-2 pb-2 pl-2">
|
||||
<div>
|
||||
<Icon type={item_type(item)} classes="h-4 w-4 flex-none mt-1 mx-1" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-primary-light-700 dark:text-primary-dark-300">
|
||||
<span class="text-primary-light-700 dark:text-primary-dark-500">
|
||||
{item.library_name}
|
||||
</span>
|
||||
{item_type(item)}
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap items-center">
|
||||
<div class="font-bold">
|
||||
{item_name(item)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{first_sentence(item)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
</LivePatch>
|
||||
{/if}
|
||||
{#if item.__struct__ != AshHq.Docs.Guide}
|
||||
<a
|
||||
class="block w-full text-left border-base-light-300 dark:border-base-dark-600"
|
||||
|
@ -103,107 +139,69 @@ defmodule AshHqWeb.Components.Search do
|
|||
phx-click={@close}
|
||||
>
|
||||
<div class={
|
||||
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-4",
|
||||
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-1 w-full",
|
||||
"bg-base-light-200 dark:bg-base-dark-700": @selected_item.id == item.id
|
||||
}>
|
||||
<div class="flex justify-start items-center space-x-2 pb-2 pl-2">
|
||||
<div>
|
||||
<Icon type={item_type(item)} classes="h-4 w-4 flex-none mt-1 mx-1" />
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap items-center">
|
||||
{#for {path_item, index} <- Enum.with_index(item_path(item))}
|
||||
{#if index != 0}
|
||||
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
|
||||
{/if}
|
||||
<div>
|
||||
{path_item}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-primary-light-700 dark:text-primary-dark-300">
|
||||
<span class="text-primary-light-700 dark:text-primary-dark-500">
|
||||
{item.library_name}
|
||||
</span>
|
||||
{item_type(item)}
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap items-center">
|
||||
<div class="font-bold">
|
||||
{item_name(item)}
|
||||
</div>
|
||||
{/for}
|
||||
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
|
||||
<div class="font-bold">
|
||||
{item_name(item)}
|
||||
</div>
|
||||
<div>
|
||||
{first_sentence(item)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base-light-700 dark:text-base-dark-400 ml-10">
|
||||
{raw(item.search_headline)}
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
{#if item.__struct__ == AshHq.Docs.Guide}
|
||||
<LivePatch
|
||||
class="block w-full text-left border-base-light-300 dark:border-base-dark-600"
|
||||
to={DocRoutes.doc_link(item)}
|
||||
opts={id: "result-#{item.id}", "phx-click": @close}
|
||||
>
|
||||
<div class={
|
||||
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-4",
|
||||
"bg-base-light-200 dark:bg-base-dark-700": @selected_item.id == item.id
|
||||
}>
|
||||
<div class="flex justify-start items-center space-x-2 pb-2 pl-2">
|
||||
<div>
|
||||
<Icon type={item_type(item)} classes="h-4 w-4 flex-none mt-1 mx-1" />
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap items-center">
|
||||
{#for {path_item, index} <- Enum.with_index(item_path(item))}
|
||||
{#if index != 0}
|
||||
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
|
||||
{/if}
|
||||
<div>
|
||||
{path_item}
|
||||
</div>
|
||||
{/for}
|
||||
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
|
||||
<div class="font-bold">
|
||||
{item_name(item)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base-light-700 dark:text-base-dark-400 ml-10">
|
||||
{raw(item.search_headline)}
|
||||
</div>
|
||||
</div>
|
||||
</LivePatch>
|
||||
{/if}
|
||||
{/for}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp item_name(%{thread_name: thread_name, channel_name: channel_name}),
|
||||
do: "#{String.capitalize(channel_name)} Forum: #{inspect(thread_name)}"
|
||||
defp first_sentence(%{text: text}), do: first_sentence(text)
|
||||
defp first_sentence(%{doc: doc}), do: first_sentence(doc)
|
||||
|
||||
defp first_sentence(doc) do
|
||||
first_sentence =
|
||||
doc
|
||||
|> String.trim()
|
||||
|> String.split("<!--- heads-end -->", parts: 2)
|
||||
|> List.last()
|
||||
|> String.trim()
|
||||
|> String.split("\n", parts: 2)
|
||||
|> Enum.at(0)
|
||||
|> String.trim()
|
||||
|
||||
if String.starts_with?(first_sentence, "`") do
|
||||
""
|
||||
else
|
||||
first_sentence
|
||||
end
|
||||
end
|
||||
|
||||
defp item_name(%AshHq.Docs.Function{call_name: call_name, arity: arity}),
|
||||
do: call_name <> "/#{arity}"
|
||||
|
||||
defp item_name(%AshHq.Docs.Option{path: path, name: name}), do: Enum.join(path ++ [name], ".")
|
||||
defp item_name(%AshHq.Docs.Dsl{path: path, name: name}), do: Enum.join(path ++ [name], ".")
|
||||
defp item_name(%{name: name}), do: name
|
||||
defp item_name(%{version: version}), do: version
|
||||
|
||||
defp item_path(%{
|
||||
library_name: library_name,
|
||||
extension_name: extension_name,
|
||||
path: path
|
||||
}) do
|
||||
[library_name, extension_name, path] |> List.flatten()
|
||||
end
|
||||
|
||||
defp item_path(%{
|
||||
library_name: library_name,
|
||||
module_name: module_name
|
||||
}) do
|
||||
[library_name, module_name]
|
||||
end
|
||||
|
||||
defp item_path(%{library_name: library_name}) do
|
||||
[library_name]
|
||||
end
|
||||
|
||||
defp item_path(%{library_version: %{library_name: library_name}}) do
|
||||
[library_name]
|
||||
end
|
||||
|
||||
defp item_path(_) do
|
||||
[]
|
||||
end
|
||||
|
||||
def mount(socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -273,19 +271,14 @@ defmodule AshHqWeb.Components.Search do
|
|||
if socket.assigns.search in [nil, ""] do
|
||||
socket
|
||||
else
|
||||
%{result: item_list} =
|
||||
AshHq.Docs.Search.run!(
|
||||
socket.assigns.search,
|
||||
%{types: socket.assigns[:selected_types]}
|
||||
)
|
||||
|
||||
item_list = Enum.take(item_list, 50)
|
||||
|
||||
selected_item = Enum.at(item_list, 0)
|
||||
item_list =
|
||||
socket.assigns.search
|
||||
|> AshHq.Docs.Indexer.search()
|
||||
|> Enum.take(50)
|
||||
|
||||
socket
|
||||
|> assign(:item_list, item_list)
|
||||
|> set_selected_item(selected_item)
|
||||
|> set_selected_item(Enum.at(item_list, 0))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,353 +0,0 @@
|
|||
defmodule AshHqWeb.Pages.Ashley do
|
||||
@moduledoc "Ashley page"
|
||||
use Surface.LiveComponent
|
||||
import AshHqWeb.Tails
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
alias Surface.Components.Form
|
||||
|
||||
alias Surface.Components.Form.{
|
||||
Field,
|
||||
TextArea,
|
||||
TextInput
|
||||
}
|
||||
|
||||
prop(current_user, :any)
|
||||
prop(params, :map)
|
||||
|
||||
data(messages, :list)
|
||||
data(message_form, :any)
|
||||
data(new_message_form, :any)
|
||||
data(conversation, :any)
|
||||
data(conversations, :list)
|
||||
data(editing_conversation, :boolean)
|
||||
data(conversation_form, :any)
|
||||
|
||||
def render(assigns) do
|
||||
~F"""
|
||||
<div class="lg:grid lg:grid-cols-6 lg:w-2/3 lg:mx-auto mx-8">
|
||||
{#if is_nil(@current_user) || !@current_user.ashley_access}
|
||||
You do not have access to this page.
|
||||
{#else}
|
||||
<div class="grid-cols-1 flex-col w-full hidden lg:block mr-2">
|
||||
<a
|
||||
href="/ashley"
|
||||
class="p-2 rounded-md bg-gray-300 dark:bg-gray-800 w-full flex flex-row items-center hover:bg-gray-500 hover:dark:bg-gray-600"
|
||||
>
|
||||
<Heroicons.Solid.PlusIcon class="h-4 w-4" /> New
|
||||
</a>
|
||||
{#for conversation <- @conversations}
|
||||
<div class={classes([
|
||||
"p-2",
|
||||
["text-gray-500": conversation.question_count >= AshHq.Ashley.Conversation.conversation_limit()]
|
||||
])}>
|
||||
<a href={"/ashley/#{conversation.id}"}>
|
||||
{conversation.name} - {conversation.question_count}</a>
|
||||
</div>
|
||||
{/for}
|
||||
</div>
|
||||
<div class="grid-cols-1 flex-col w-full block lg:hidden">
|
||||
<div class="flex flex-row items-center justify-end mb-4">
|
||||
<a
|
||||
href="/ashley"
|
||||
class="p-2 rounded-md bg-gray-300 dark:bg-gray-800 flex flex-row items-center w-24 h-12"
|
||||
>
|
||||
<Heroicons.Solid.PlusIcon class="h-4 w-4" /> New
|
||||
</a>
|
||||
<button
|
||||
phx-click={JS.toggle(to: "#mobile-conversations")}
|
||||
class="p-2 rounded-md bg-gray-300 dark:bg-gray-800 w-12 h-12 ml-4 flex flex-row justify-center items-center"
|
||||
>
|
||||
<Heroicons.Outline.MenuIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col" id="mobile-conversations" style="display: none;">
|
||||
{#for conversation <- @conversations}
|
||||
<div class={classes([
|
||||
"p-2",
|
||||
["text-gray-500": conversation.question_count >= AshHq.Ashley.Conversation.conversation_limit()]
|
||||
])}>
|
||||
<a href={"/ashley/#{conversation.id}"}>
|
||||
{conversation.name} - {conversation.question_count}</a>
|
||||
</div>
|
||||
{/for}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="chat-window"
|
||||
class="col-span-5 flex-col prose prose-xl dark:prose-invert h-screen w-full"
|
||||
>
|
||||
{#if @conversation}
|
||||
{#if @editing_conversation}
|
||||
<Form for={@conversation_form} submit="save_conversation" class="flex flex-row justify-between">
|
||||
<Field name={:name} class="w-full">
|
||||
<TextInput class="flex-grow text-black block focus:ring-primary-light-600 focus:primary-light-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300 w-full" />
|
||||
</Field>
|
||||
<button type="submit">
|
||||
<Heroicons.Outline.SaveIcon class="h-5 w-5" />
|
||||
</button>
|
||||
</Form>
|
||||
{#else}
|
||||
<div class="flex flex-row w-full justify-between">
|
||||
<div>
|
||||
{@conversation.name}
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<button :on-click="toggle-editing-conversation" class="mr-4">
|
||||
<Heroicons.Outline.PencilIcon class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
:on-click="destroy-conversation"
|
||||
phx-value-conversation-id={@conversation.id}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
<Heroicons.Outline.XIcon class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="overflow-y-scroll h-2/3">
|
||||
<div style="scroll-snap-type: y; scroll-snap-align: end;">
|
||||
<div>
|
||||
<div>
|
||||
Hello! My name is Ashley. I've been instructed to answer your questions as factually as possible, but I am *far* from perfect.
|
||||
My code snippets especially are not likely to be accurate. However, I cite my sources below each answer to show you what content
|
||||
I thought was relevant, so please use that for official clarification.
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
{#for question <- @conversation.questions}
|
||||
<div>
|
||||
<div class="font-light mt-12">
|
||||
{question.question}
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
{raw(question.answer_html)}
|
||||
</div>
|
||||
{#if question.sources != []}
|
||||
<h3>Sources</h3>
|
||||
<ul>
|
||||
{#for source <- question.sources}
|
||||
<li><a href={"https://ash-hq.org/#{source.link}"}>{source.name}</a></li>
|
||||
{/for}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/for}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-12 w-full">
|
||||
<Form for={@message_form} submit="save_message">
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="w-full">
|
||||
<span class="text-sm font-extralight">{@conversation.question_count} of {AshHq.Ashley.Conversation.conversation_limit()} used</span>
|
||||
<Field name={:question} class="w-full">
|
||||
<TextArea
|
||||
class="flex-grow text-black block focus:ring-primary-light-600 focus:primary-light-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300 w-full"
|
||||
opts={disabled: @conversation.question_count >= AshHq.Ashley.Conversation.conversation_limit()}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="p-4 rounded-xl bg-gray-200 dark:bg-gray-600 flex flex-row items-center ml-12 h-12"
|
||||
>
|
||||
<Heroicons.Outline.PaperAirplaneIcon class="h-4 w-4" /><div>Submit</div>
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
{#else}
|
||||
<Form for={@new_message_form} submit="save_new_message">
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="w-full">
|
||||
<Field name={:conversation_name} class="mb-4">
|
||||
<TextInput
|
||||
class="flex-grow text-black block focus:ring-primary-light-600 focus:primary-light-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300 w-full"
|
||||
opts={placeholder: "New Conversation"}
|
||||
/>
|
||||
</Field>
|
||||
<div>
|
||||
<div>
|
||||
Hello! My name is Ashley. I've been instructed to answer your questions as factually as possible, but I am *far* from perfect.
|
||||
My code snippets especially are not likely to be accurate. However, I cite my sources below each answer to show you what content
|
||||
I thought was relevant, so please use that for official clarification.
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="flex flex-col space-y-4 lg:flex-row lg:justify-between items-center justify-center">
|
||||
<Field name={:question} class="w-full">
|
||||
<TextArea class="flex-grow text-black block focus:ring-primary-light-600 focus:primary-light-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300 w-full" />
|
||||
</Field>
|
||||
<button
|
||||
type="submit_new_message"
|
||||
class="p-4 rounded-xl bg-gray-200 dark:bg-gray-600 flex flex-row items-center lg:ml-12 h-12"
|
||||
>
|
||||
<Heroicons.Outline.PaperAirplaneIcon class="h-4 w-4" /><div>Submit</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def update(assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(editing_conversation: false)
|
||||
|> assign_conversations()
|
||||
|> assign_conversation()
|
||||
|> assign_message_form()
|
||||
|> assign_change_name_form()
|
||||
|> assign_new_message_form}
|
||||
end
|
||||
|
||||
defp assign_change_name_form(socket) do
|
||||
if socket.assigns[:conversation] do
|
||||
assign(
|
||||
socket,
|
||||
:conversation_form,
|
||||
AshPhoenix.Form.for_update(socket.assigns.conversation, :update,
|
||||
actor: socket.assigns.current_user,
|
||||
api: AshHq.Ashley,
|
||||
as: "conversation"
|
||||
)
|
||||
)
|
||||
else
|
||||
assign(socket, conversation_form: nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_conversations(socket) do
|
||||
if socket.assigns[:current_user] do
|
||||
assign(socket,
|
||||
conversations:
|
||||
AshHq.Ashley.Conversation.read!(
|
||||
actor: socket.assigns.current_user,
|
||||
load: :question_count
|
||||
)
|
||||
)
|
||||
else
|
||||
assign(socket, conversations: [])
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_conversation(socket) do
|
||||
if socket.assigns[:current_user] && socket.assigns[:params]["conversation_id"] do
|
||||
assign(socket,
|
||||
conversation:
|
||||
Enum.find(
|
||||
socket.assigns.conversations,
|
||||
&(&1.id == socket.assigns[:params]["conversation_id"])
|
||||
)
|
||||
|> AshHq.Ashley.load!(:questions, actor: socket.assigns.current_user)
|
||||
)
|
||||
else
|
||||
assign(socket, :conversation, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_message_form(socket) do
|
||||
if socket.assigns[:current_user] && socket.assigns[:conversation] do
|
||||
assign(
|
||||
socket,
|
||||
:message_form,
|
||||
AshPhoenix.Form.for_create(AshHq.Ashley.Question, :ask,
|
||||
actor: socket.assigns[:current_user],
|
||||
api: AshHq.Ashley,
|
||||
as: "message",
|
||||
prepare_source: fn changeset ->
|
||||
Ash.Changeset.force_change_attribute(
|
||||
changeset,
|
||||
:conversation_id,
|
||||
socket.assigns.conversation.id
|
||||
)
|
||||
end
|
||||
)
|
||||
)
|
||||
else
|
||||
assign(socket, :message_form, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_new_message_form(socket) do
|
||||
if socket.assigns[:current_user] do
|
||||
assign(
|
||||
socket,
|
||||
:new_message_form,
|
||||
AshPhoenix.Form.for_create(AshHq.Ashley.Question, :ask,
|
||||
actor: socket.assigns[:current_user],
|
||||
api: AshHq.Ashley,
|
||||
as: "new_message"
|
||||
)
|
||||
)
|
||||
else
|
||||
assign(socket, :new_message_form, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("save_conversation", %{"conversation" => conversation_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.conversation_form, params: conversation_params) do
|
||||
{:ok, _message} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_conversations()
|
||||
|> assign_conversation()
|
||||
|> assign(editing_conversation: false)
|
||||
|> assign_message_form()}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, conversation_form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("save_message", %{"message" => message_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.message_form, params: message_params) do
|
||||
{:ok, _message} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_conversations()
|
||||
|> assign_conversation()
|
||||
|> assign_message_form()}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, message_form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("save_new_message", %{"new_message" => message_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.new_message_form, params: message_params) do
|
||||
{:ok, message} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> push_patch(to: "/ashley/#{message.conversation_id}")}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, message_form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("toggle-editing-conversation", _, socket) do
|
||||
{:noreply, assign(socket, editing_conversation: true)}
|
||||
end
|
||||
|
||||
def handle_event("destroy-conversation", %{"conversation-id" => conversation_id}, socket) do
|
||||
case Enum.split_with(socket.assigns.conversations, &(&1.id == conversation_id)) do
|
||||
{[conversation], rest} ->
|
||||
AshHq.Ashley.Conversation.destroy!(conversation, actor: socket.assigns.current_user)
|
||||
{:noreply, assign(socket, conversations: rest, conversation: nil)}
|
||||
|
||||
_ ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ defmodule AshHqWeb.AppViewLive do
|
|||
alias AshHqWeb.Components.AppView.TopBar
|
||||
|
||||
alias AshHqWeb.Components.Search
|
||||
alias AshHqWeb.Pages.{Ashley, Blog, Community, Docs, Home, Media, UserSettings}
|
||||
alias AshHqWeb.Pages.{Blog, Community, Docs, Home, Media, UserSettings}
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
alias Surface.Components.Context
|
||||
|
@ -111,11 +111,9 @@ defmodule AshHqWeb.AppViewLive do
|
|||
<UserSettings id="user_settings" current_user={@current_user} />
|
||||
{#match :media}
|
||||
<Media id="media" />
|
||||
{#match :ashley}
|
||||
<Ashley id="ashley" current_user={@current_user} params={@params} />
|
||||
{/case}
|
||||
|
||||
{#if @live_action not in [:docs_dsl, :ashley]}
|
||||
{#if @live_action != :docs_dsl}
|
||||
<footer class="p-8 sm:p-6 bg-base-light-200 dark:bg-base-dark-850 sm:justify-center sticky">
|
||||
<div class="md:flex md:justify-around">
|
||||
<div class="flex justify-center flex-row mb-6 md:mb-0">
|
||||
|
|
53
litefs.yml
Normal file
53
litefs.yml
Normal file
|
@ -0,0 +1,53 @@
|
|||
# The fuse section describes settings for the FUSE file system. This file system
|
||||
# is used as a thin layer between the SQLite client in your application and the
|
||||
# storage on disk. It intercepts disk writes to determine transaction boundaries
|
||||
# so that those transactions can be saved and shipped to replicas.
|
||||
fuse:
|
||||
dir: "/litefs"
|
||||
|
||||
# The data section describes settings for the internal LiteFS storage. We'll
|
||||
# mount a volume to the data directory so it can be persisted across restarts.
|
||||
# However, this data should not be accessed directly by the user application.
|
||||
data:
|
||||
dir: "/var/lib/litefs"
|
||||
|
||||
# This flag ensure that LiteFS continues to run if there is an issue on starup.
|
||||
# It makes it easy to ssh in and debug any issues you might be having rather
|
||||
# than continually restarting on initialization failure.
|
||||
exit-on-error: false
|
||||
|
||||
# This section defines settings for the option HTTP proxy.
|
||||
# This proxy can handle primary forwarding & replica consistency
|
||||
# for applications that use a single SQLite database.
|
||||
proxy:
|
||||
addr: ":8080"
|
||||
target: "localhost:4000"
|
||||
db: "db"
|
||||
passthrough:
|
||||
- "*.ico"
|
||||
- "*.png"
|
||||
|
||||
# This section defines a list of commands to run after LiteFS has connected
|
||||
# and sync'd with the cluster. You can run multiple commands but LiteFS expects
|
||||
# the last command to be long-running (e.g. an application server). When the
|
||||
# last command exits, LiteFS is shut down.
|
||||
exec:
|
||||
# Only run migrations on candidate nodes.
|
||||
- cmd: "./_build/prod/rel/ash_hq/bin/ash_hq eval 'AshHq.Release.migrate'"
|
||||
if-candidate: true
|
||||
|
||||
- cmd: "./_build/prod/rel/ash_hq/bin/ash_hq start"
|
||||
|
||||
# The lease section specifies how the cluster will be managed. We're using the
|
||||
# "consul" lease type so that our application can dynamically change the primary.
|
||||
#
|
||||
# These environment variables will be available in your Fly.io application.
|
||||
lease:
|
||||
type: "consul"
|
||||
advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202"
|
||||
candidate: ${FLY_REGION == PRIMARY_REGION}
|
||||
promote: true
|
||||
|
||||
consul:
|
||||
url: "${FLY_CONSUL_URL}"
|
||||
key: "litefs/${FLY_APP_NAME}"
|
9
mix.exs
9
mix.exs
|
@ -41,6 +41,7 @@ defmodule AshHq.MixProject do
|
|||
[
|
||||
{:ash, github: "ash-project/ash", override: true},
|
||||
{:ash_postgres, github: "ash-project/ash_postgres"},
|
||||
{:ash_sqlite, github: "ash-project/ash_sqlite"},
|
||||
{:ash_admin, github: "ash-project/ash_admin"},
|
||||
{:ash_phoenix, github: "ash-project/ash_phoenix", override: true},
|
||||
{:ash_graphql, github: "ash-project/ash_graphql"},
|
||||
|
@ -50,6 +51,7 @@ defmodule AshHq.MixProject do
|
|||
{:ash_authentication_phoenix, "~> 1.6"},
|
||||
{:ash_blog, github: "ash-project/ash_blog"},
|
||||
{:ash_csv, github: "ash-project/ash_csv"},
|
||||
{:spark, github: "ash-project/spark", override: true},
|
||||
# HTTP calls
|
||||
{:req, "~> 0.4.3"},
|
||||
# Appsignal
|
||||
|
@ -59,11 +61,8 @@ defmodule AshHq.MixProject do
|
|||
# Discord
|
||||
{:nostrum, github: "zachdaniel/nostrum"},
|
||||
{:cowlib, "~> 2.11", hex: :remedy_cowlib, override: true},
|
||||
# Ashley
|
||||
{:open_ai, github: "hernanat/open_ai_ex"},
|
||||
{:pinecone, "~> 0.1.0"},
|
||||
# Tiktoken isn't working for some reason
|
||||
# {:tiktoken, "~> 0.1.0"}
|
||||
# Search
|
||||
{:haystack, "~> 0.1.0"},
|
||||
# Clustering
|
||||
{:libcluster, "~> 3.3"},
|
||||
# UI
|
||||
|
|
19
mix.lock
19
mix.lock
|
@ -4,7 +4,7 @@
|
|||
"appsignal": {:hex, :appsignal, "2.7.9", "efc11601a848f153752778356bc86f9af03e925f15a961f714cd702a83cee434", [:make, :mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:decorator, "~> 1.2.3 or ~> 1.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "84fff1dcba3d3fcf542c528ffb0f4eba891da0f68125fcf57e9b30d1a1a4a6e9"},
|
||||
"appsignal_phoenix": {:hex, :appsignal_phoenix, "2.3.4", "c83a8e15a51456db7d722a21bfe9a45e23618b550219caa8fb6d4853f61b5734", [:mix], [{:appsignal, ">= 2.7.6 and < 3.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:appsignal_plug, ">= 2.0.15 and < 3.0.0", [hex: :appsignal_plug, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.11 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.9", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1ca040fbfa653bdda25735031d8b89c81ebb50c475bc35d5ac0e13236e5f6600"},
|
||||
"appsignal_plug": {:hex, :appsignal_plug, "2.0.15", "758a8a78944878e8461bbc77ca86219121a56f4299c6d79940ab083cf9afea00", [:mix], [{:appsignal, ">= 2.7.6 and < 3.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c6059049e2081e808aaef04e2b9917e06277f61a35a0e103db860d08cbc41f1"},
|
||||
"ash": {:git, "https://github.com/ash-project/ash.git", "74fbd8a6fe85be3f4b61682a824f0e4d2dc9cabd", []},
|
||||
"ash": {:git, "https://github.com/ash-project/ash.git", "b93cbb95775d3e5f9166167134cafea26a94d2fc", []},
|
||||
"ash_admin": {:git, "https://github.com/ash-project/ash_admin.git", "3002af9ec69dc475582ef5f445064e4594bf45ac", []},
|
||||
"ash_appsignal": {:hex, :ash_appsignal, "0.1.2", "a6eb1927a13c11006aad0d9ffaa011143344dd04c9b07ab94f459498b8ddc6d4", [:mix], [{:appsignal, "~> 2.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:ash, ">= 2.14.14", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "dae3158337d2a36b76f04519ebe6d08ef5296823831993cef6069eeb879c5b94"},
|
||||
"ash_authentication": {:hex, :ash_authentication, "3.11.15", "7834446cdd13bb471bded630aa0e0e4fb8795ffffe0c294dc22448d3778ff035", [:mix], [{:ash, ">= 2.5.11 and < 3.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:assent, "~> 0.2", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.16.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, ">= 1.1.39 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "931926d8dd3fc5ac54a354d69aad6aed30438977d7971bf3fdfae4c563557d21"},
|
||||
|
@ -14,12 +14,13 @@
|
|||
"ash_graphql": {:git, "https://github.com/ash-project/ash_graphql.git", "cf57747d79f848608252ab167d74bb59aaef2345", []},
|
||||
"ash_json_api": {:git, "https://github.com/ash-project/ash_json_api.git", "31ece4fad9920c7e45c600d38ac82218296c4612", []},
|
||||
"ash_phoenix": {:git, "https://github.com/ash-project/ash_phoenix.git", "35e4d2931e1664383c9a085a90f846e58986c8c8", []},
|
||||
"ash_postgres": {:git, "https://github.com/ash-project/ash_postgres.git", "3e2826db04ad537137f99a69bcf275bc16f75b18", []},
|
||||
"ash_postgres": {:git, "https://github.com/ash-project/ash_postgres.git", "41c34b8777777a116ea87fe93d3b5603ef9ac030", []},
|
||||
"ash_sqlite": {:git, "https://github.com/ash-project/ash_sqlite.git", "b4ed3806c4ef51c06c682956f24fb2895a0a7e54", []},
|
||||
"assent": {:hex, :assent, "0.2.7", "aa68f68e577077c091ce722bff8fe1ae56b95b274bb8107f7a5406cc15a65da7", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "08106af439de4f9de114c0334de4c848de7cfbe53a5a52d342a784c4f6bc86f3"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
|
||||
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.8", "933a5f4da3b19ee56539a076076ce4d7716d64efc8db46fd066996a7e46e2bfd", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "176bdf4366956e456bf761b54ad70bc4103d0269ca9558fd7cee93d1b3f116db"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
"chacha20": {:hex, :chacha20, "1.0.4", "0359d8f9a32269271044c1b471d5cf69660c362a7c61a98f73a05ef0b5d9eb9e", [:mix], [], "hexpm", "2027f5d321ae9903f1f0da7f51b0635ad6b8819bc7fe397837930a2011bc2349"},
|
||||
"cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"},
|
||||
|
@ -43,6 +44,7 @@
|
|||
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [: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", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
||||
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.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", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
|
||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.12.0", "9ee845ac45a76e3c5c0fe65898f3538f5b0969912a95f0beef3d4ae8e63f6a06", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "4eaf8550df1fd0043bcf039a5dce407fd8afc30a115ced173fe6b9815eeedb55"},
|
||||
"ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"},
|
||||
"eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
|
||||
|
@ -53,6 +55,7 @@
|
|||
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [: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", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"},
|
||||
"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"},
|
||||
"exqlite": {:hex, :exqlite, "0.15.0", "efa87268ffb648a29d887d6b5641cea6d0c7cdc09b83e92a34377ab5b96913c1", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "bf704989342b99ea5e1621495fb94fc772516206550d3b2465a1de3d89345853"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
|
||||
"floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"},
|
||||
|
@ -62,8 +65,10 @@
|
|||
"git_ops": {:hex, :git_ops, "2.5.1", "94ab6e3bc69fe765a62cbdb09969016613a154dec8fc4f6ebae682f030451da9", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "1219edc8810dcea40472ec5b7ed04786a9e1b0e4e49d8642b0e1cdfb8a6ad261"},
|
||||
"gun": {:hex, :remedy_gun, "2.0.1", "0f0caed812ed9e4da4f144df2d5bf73b0a99481d395ecde990a3791decf321c6", [:rebar3], [{:cowlib, "~> 2.11.1", [hex: :remedy_cowlib, repo: "hexpm", optional: false]}], "hexpm", "b6685a85fbd12b757f86809be1b3d88fcef365b77605cd5aa34db003294c446e"},
|
||||
"hackney": {:hex, :hackney, "1.19.1", "59de4716e985dd2b5cbd4954fa1ae187e2b610a9c4520ffcb0b1653c3d6e5559", [:rebar3], [{:certifi, "~> 2.12.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.4.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", "8aa08234bdefc269995c63c2282cf3cd0e36febe3a6bfab11b610572fdd1cad0"},
|
||||
"haystack": {:hex, :haystack, "0.1.0", "6cb9c72caf40ed4a5f9a094e6b09993c68c3fda0e01280c60c331a19860c504c", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:stemmer, "~> 1.1", [hex: :stemmer, repo: "hexpm", optional: false]}], "hexpm", "27a582513ef933c1b11345b96f8d41ee137d03b25312bd85068ffe8fec503635"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||
"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.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},
|
||||
|
@ -79,16 +84,15 @@
|
|||
"makeup_html": {:hex, :makeup_html, "0.1.0", "b0228fda985e311d8f0d25bed58f8280826633a38d7448cabdd723e116165bcf", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "0ca44e7dcb8d933e010740324470dd8ec947243b51304bd34b8165ef3281edc2"},
|
||||
"makeup_js": {:hex, :makeup_js, "0.1.0", "ffa8ce9db95d14dcd09045334539d5992d540d63598c592d4805b7674bdd6675", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "3f0c1a5eb52c9737b1679c926574e83bb260ccdedf08b58ee96cca7c685dea75"},
|
||||
"makeup_sql": {:hex, :makeup_sql, "0.1.0", "197a8a0a38e83885f73767530739bb8f990aecf7fd1597d3141608c14f5f233e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "556e23ff88ad2fb8c44e393467cfba0c4f980cbe90316deaf48a1362f58cd118"},
|
||||
"meilisearch": {:hex, :meilisearch, "0.20.0", "3606a7dc9c51b8e7d890761a9614dd56812da8bfcbf4e86934ae42233b257b18", [:mix], [{:httpoison, "~> 1.8", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "50faca2a72128bc93e6771bc66dadc347357c0a2d619e743e620ce131c7798fd"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
|
||||
"multipart": {:hex, :multipart, "0.3.1", "886d77125f5d7ba6be2f86e4be8f6d3556684c8e56a777753f06234885b09cde", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "9657783995d2b9b546d9c66e1d497fcb473d813a8a3fb73faf5e411538b1db97"},
|
||||
"nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||
"nostrum": {:git, "https://github.com/zachdaniel/nostrum.git", "00110f97fb2a9534cf506bca01b79fd092b32581", []},
|
||||
"open_ai": {:git, "https://github.com/hernanat/open_ai_ex.git", "e95b6a7b748947a0783c6cb38edbbfc209f1f42d", []},
|
||||
"open_api_spex": {:hex, :open_api_spex, "3.16.3", "11bc9798890073e516a97392d5846a235925e48ecbb468cb5b1cc207d5785a3e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "1bcbe6efab88f5d001c2fc377e0bd6058180aa31b68d32962d4926e934b8ecad"},
|
||||
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
|
@ -102,7 +106,6 @@
|
|||
"phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||
"pinecone": {:hex, :pinecone, "0.1.0", "65c14c7f178fa97350db65685247ba5de641f9379d1e0b78c77183dc2fe7d199", [:mix], [{:bypass, "~> 2.1", [hex: :bypass, repo: "hexpm", optional: false]}, {:tesla, "~> 1.4", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "0212d5f0134cb724d5b6cfd224bcd739fd075c0b6271d0bfc0f4fc0f31b48f13"},
|
||||
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [: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", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
|
||||
"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.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
|
||||
|
@ -116,8 +119,9 @@
|
|||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
|
||||
"sourceror": {:hex, :sourceror, "0.14.0", "b6b8552d0240400d66b6f107c1bab7ac1726e998efc797f178b7b517e928e314", [:mix], [], "hexpm", "809c71270ad48092d40bbe251a133e49ae229433ce103f762a2373b7a10a8d8b"},
|
||||
"spark": {:hex, :spark, "1.1.45", "85d02ed5623079a304dbff2ff63bc5cac37301005f7cb72e727f1d1dbdcd5db3", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "538d7ff671f19a086458147eb04c38251175c60afed204231fb4848e8cdfb378"},
|
||||
"spark": {:git, "https://github.com/ash-project/spark.git", "92ba4bd227a2f0d4f55d5a2438e8af6039913fbc", []},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"stemmer": {:hex, :stemmer, "1.1.0", "71221331ced40832b47e6989a12dd9de1b15c982043d1014742be83c34ec9e79", [:mix], [], "hexpm", "0cb5faf73476b84500e371ff39fd9a494f60ab31d991689c1cd53b920556228f"},
|
||||
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
|
||||
"sunflower_ui": {:git, "https://github.com/zachdaniel/sunflower_ui.git", "3ec87f33e003693e6db2329f9d6d8ac59983cf17", []},
|
||||
"surface": {:hex, :surface, "0.9.4", "60307da284715de50a21ff0f78a27d4c7db48e88afddd3794319bfc534c0051e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.14", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.11", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "a29312c6346f0662dbf8d1d364a0fbf9f53fc469c375bf2b1efcbd0f1bc9844c"},
|
||||
|
@ -128,7 +132,6 @@
|
|||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"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"},
|
||||
"tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"},
|
||||
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
|
|
32
priv/repo/migrations/20231012101707_migrate_resources58.exs
Normal file
32
priv/repo/migrations/20231012101707_migrate_resources58.exs
Normal file
|
@ -0,0 +1,32 @@
|
|||
defmodule AshHq.Repo.Migrations.MigrateResources58 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
|
||||
execute("""
|
||||
ALTER TABLE discord_messages
|
||||
DROP COLUMN searchable
|
||||
""")
|
||||
|
||||
drop_if_exists(
|
||||
index(:discord_messages, ["searchable"], name: "discord_messages_searchable_index")
|
||||
)
|
||||
end
|
||||
|
||||
def down do
|
||||
create index(:discord_messages, ["searchable"], using: "GIN")
|
||||
|
||||
execute("""
|
||||
ALTER TABLE discord_messages
|
||||
ADD COLUMN searchable tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', content), 'D')
|
||||
) STORED;
|
||||
""")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "bigint",
|
||||
"source": "id",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"primary_key?": true,
|
||||
"generated?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "author",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"primary_key?": false,
|
||||
"generated?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "content",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"primary_key?": false,
|
||||
"generated?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "content_html",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"primary_key?": false,
|
||||
"generated?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "utc_datetime",
|
||||
"source": "timestamp",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"primary_key?": false,
|
||||
"generated?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "bigint",
|
||||
"source": "thread_id",
|
||||
"references": {
|
||||
"name": "discord_messages_thread_id_fkey",
|
||||
"table": "discord_threads",
|
||||
"schema": "public",
|
||||
"destination_attribute": "id",
|
||||
"on_delete": "delete",
|
||||
"on_update": "update",
|
||||
"deferrable": false,
|
||||
"primary_key?": true,
|
||||
"multitenancy": {
|
||||
"global": null,
|
||||
"strategy": null,
|
||||
"attribute": null
|
||||
},
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null
|
||||
},
|
||||
"allow_nil?": false,
|
||||
"primary_key?": false,
|
||||
"generated?": false
|
||||
}
|
||||
],
|
||||
"table": "discord_messages",
|
||||
"hash": "52BCA98DD8B99880A3BD22A6CF45EC177E9BE8F20FCE47D4D961EF056C0A2C06",
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"identities": [],
|
||||
"custom_indexes": [],
|
||||
"base_filter": null,
|
||||
"custom_statements": [],
|
||||
"multitenancy": {
|
||||
"global": null,
|
||||
"strategy": null,
|
||||
"attribute": null
|
||||
},
|
||||
"has_create_action": true,
|
||||
"check_constraints": []
|
||||
}
|
|
@ -192,4 +192,4 @@
|
|||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -192,4 +192,4 @@
|
|||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,4 +219,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -239,4 +239,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,4 +249,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,4 +249,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,4 +249,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,4 +249,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -269,4 +269,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -292,4 +292,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -282,4 +282,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,4 +285,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,4 +285,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -298,4 +298,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "dsls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -301,4 +301,4 @@
|
|||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"has_create_action": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -301,4 +301,4 @@
|
|||
}
|
||||
],
|
||||
"has_create_action": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,4 +127,4 @@
|
|||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,4 +127,4 @@
|
|||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,4 +149,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,4 +159,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,4 +159,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,4 +159,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -179,4 +179,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,4 +182,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,4 +182,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -192,4 +192,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "extensions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -166,4 +166,4 @@
|
|||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,4 +143,4 @@
|
|||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,4 +143,4 @@
|
|||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,4 +146,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -166,4 +166,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,4 +186,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -196,4 +196,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -196,4 +196,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -209,4 +209,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -229,4 +229,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,4 +232,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,4 +232,4 @@
|
|||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "functions"
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue