Compare commits

..

5 Commits

21 changed files with 1002 additions and 12 deletions

View File

@@ -2,7 +2,7 @@
// Do not edit this file manually
import type { AshRpcError, ConditionalPaginatedResultMixed, InferResult, SortString, UUID, UnifiedFieldSelection, ValidationResult, tweetsFilterInput, tweetsResourceSchema, tweetsSortField } from "./ash_types";
import type { AshRpcError, ConditionalPaginatedResultMixed, InferResult, SortString, UUID, UnifiedFieldSelection, ValidationResult, mediaFilterInput, mediaResourceSchema, mediaSortField, tweetsFilterInput, tweetsResourceSchema, tweetsSortField } from "./ash_types";
export type * from "./ash_types";
// Helper Functions
@@ -201,8 +201,110 @@ export async function executeValidationRpcRequest<T>(
export type ReadMediaFields = UnifiedFieldSelection<mediaResourceSchema>[];
export type InferReadMediaResult<
Fields extends ReadMediaFields | undefined,
Page extends ReadMediaConfig["page"] = undefined
> = ConditionalPaginatedResultMixed<Page, Array<InferResult<mediaResourceSchema, Fields>>, {
results: Array<InferResult<mediaResourceSchema, Fields>>;
hasMore: boolean;
limit: number;
offset: number;
count?: number | null;
type: "offset";
}, {
results: Array<InferResult<mediaResourceSchema, Fields>>;
hasMore: boolean;
limit: number;
after: string | null;
before: string | null;
previousPage: string;
nextPage: string;
count?: number | null;
type: "keyset";
}>;
export type ReadMediaConfig = {
tenant?: string;
fields: ReadMediaFields;
filter?: mediaFilterInput;
sort?: SortString<mediaSortField> | SortString<mediaSortField>[];
page?: (
{
limit?: number;
offset?: number;
count?: boolean;
} | {
limit?: number;
after?: string;
before?: string;
}
);
headers?: Record<string, string>;
fetchOptions?: RequestInit;
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
};
export type ReadMediaResult<Fields extends ReadMediaFields, Page extends ReadMediaConfig["page"] = undefined> = | { success: true; data: InferReadMediaResult<Fields, Page>; }
| { success: false; errors: AshRpcError[]; }
;
/**
* Read Media records
*
* @ashActionType :read
*/
export async function readMedia<Fields extends ReadMediaFields, Config extends ReadMediaConfig = ReadMediaConfig>(
config: Config & { fields: Fields }
): Promise<ReadMediaResult<Fields, Config["page"]>> {
const payload = {
action: "read_media",
...(config.tenant !== undefined && { tenant: config.tenant }),
...(config.fields !== undefined && { fields: config.fields }),
...(config.filter && { filter: config.filter }),
...(config.sort && { sort: Array.isArray(config.sort) ? config.sort.join(",") : config.sort }),
...(config.page && { page: config.page })
};
return executeActionRpcRequest<ReadMediaResult<Fields, Config["page"]>>(
payload,
config
);
}
/**
* Validate: Read Media records
*
* @ashActionType :read
* @validation true
*/
export async function validateReadMedia(
config: {
tenant?: string;
headers?: Record<string, string>;
fetchOptions?: RequestInit;
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}
): Promise<ValidationResult> {
const payload = {
action: "read_media",
...(config.tenant !== undefined && { tenant: config.tenant })
};
return executeValidationRpcRequest<ValidationResult>(
payload,
config
);
}
export type CreateTweetInput = {
content: string;
mediaId?: UUID;
};
export type CreateTweetFields = UnifiedFieldSelection<tweetsResourceSchema>[];
@@ -436,6 +538,7 @@ export async function validateReadTweet(
export type UpdateTweetInput = {
content?: string;
likes?: number;
userId?: UUID;
state?: "posted" | "drafted";
};

View File

@@ -5,28 +5,88 @@
export type UUID = string;
// media Schema
export type mediaResourceSchema = {
__type: "Resource";
__primitiveFields: "id" | "s3Key" | "userId" | "tweetId";
id: UUID;
s3Key: string;
userId: UUID;
tweetId: UUID | null;
tweet: { __type: "Relationship"; __resource: tweetsResourceSchema | null; };
};
export type mediaAttributesOnlySchema = {
__type: "Resource";
__primitiveFields: "id" | "s3Key" | "userId" | "tweetId";
id: UUID;
s3Key: string;
userId: UUID;
tweetId: UUID | null;
};
// tweets Schema
export type tweetsResourceSchema = {
__type: "Resource";
__primitiveFields: "id" | "content" | "userId" | "state";
__primitiveFields: "id" | "content" | "likes" | "userId" | "state";
id: UUID;
content: string;
likes: number;
userId: UUID;
state: "posted" | "drafted";
media: { __type: "Relationship"; __array: true; __resource: mediaResourceSchema; };
};
export type tweetsAttributesOnlySchema = {
__type: "Resource";
__primitiveFields: "id" | "content" | "userId" | "state";
__primitiveFields: "id" | "content" | "likes" | "userId" | "state";
id: UUID;
content: string;
likes: number;
userId: UUID;
state: "posted" | "drafted";
};
export type mediaFilterInput = {
and?: Array<mediaFilterInput>;
or?: Array<mediaFilterInput>;
not?: Array<mediaFilterInput>;
id?: {
eq?: UUID;
notEq?: UUID;
in?: Array<UUID>;
};
s3Key?: {
eq?: string;
notEq?: string;
in?: Array<string>;
};
userId?: {
eq?: UUID;
notEq?: UUID;
in?: Array<UUID>;
};
tweetId?: {
eq?: UUID;
notEq?: UUID;
in?: Array<UUID>;
isNil?: boolean;
};
tweet?: tweetsFilterInput;
};
export type tweetsFilterInput = {
and?: Array<tweetsFilterInput>;
or?: Array<tweetsFilterInput>;
@@ -44,6 +104,16 @@ export type tweetsFilterInput = {
in?: Array<string>;
};
likes?: {
eq?: number;
notEq?: number;
greaterThan?: number;
greaterThanOrEqual?: number;
lessThan?: number;
lessThanOrEqual?: number;
in?: Array<number>;
};
userId?: {
eq?: UUID;
notEq?: UUID;
@@ -57,15 +127,22 @@ export type tweetsFilterInput = {
};
media?: mediaFilterInput;
};
export const tweetsFilterFields = ["id", "content", "userId", "state", "user"] as const;
export const mediaFilterFields = ["id", "s3Key", "userId", "tweetId", "user", "tweet"] as const;
export type mediaFilterField = (typeof mediaFilterFields)[number];
export const tweetsFilterFields = ["id", "content", "likes", "userId", "state", "user", "media"] as const;
export type tweetsFilterField = (typeof tweetsFilterFields)[number];
export const tweetsSortFields = ["id", "content", "userId", "state"] as const;
export const mediaSortFields = ["id", "s3Key", "userId", "tweetId"] as const;
export type mediaSortField = (typeof mediaSortFields)[number];
export const tweetsSortFields = ["id", "content", "likes", "userId", "state"] as const;
export type tweetsSortField = (typeof tweetsSortFields)[number];

View File

@@ -14,6 +14,7 @@ import {
updateTweet,
buildCSRFHeaders,
} from "./ash_rpc";
import { uploadFile } from "./upload";
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 10_000 } },
@@ -21,7 +22,8 @@ const queryClient = new QueryClient({
// ── Types ──────────────────────────────────────────────────────────────────────
type Tweet = { id: string; content: string; userId: string; state: string };
type MediaItem = { id: string; s3Key: string };
type Tweet = { id: string; content: string; userId: string; state: string; media?: MediaItem[] };
// ── Auth context ───────────────────────────────────────────────────────────────
@@ -33,6 +35,11 @@ function timeAgo(): string {
return "just now";
}
function getAssetHost(): string {
const appEl = document.getElementById("app");
return appEl?.dataset.assetHost ?? "http://localhost:3901";
}
// ── Components ─────────────────────────────────────────────────────────────────
function Spinner() {
@@ -67,14 +74,20 @@ function CharCount({ current, max }: { current: number; max: number }) {
function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
const [text, setText] = useState("");
const [error, setError] = useState<string | null>(null);
const [pendingFile, setPendingFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [mediaId, setMediaId] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const qc = useQueryClient();
const MAX = 280;
const mutation = useMutation({
mutationFn: async (content: string) => {
const res = await createTweet({
input: { content },
input: { content, mediaId: mediaId ?? undefined },
fields: ["id", "content", "userId", "state"],
headers: buildCSRFHeaders(),
});
@@ -85,11 +98,52 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
qc.invalidateQueries({ queryKey: ["tweets"] });
setText("");
setError(null);
setMediaId(null);
setPendingFile(null);
setUploadError(null);
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
}
onSuccess?.();
},
onError: (e: Error) => setError(e.message),
});
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
// Reset the input so the same file can be re-selected after removal
e.target.value = "";
// Revoke any previous object URL to avoid memory leaks
if (previewUrl) URL.revokeObjectURL(previewUrl);
const localUrl = URL.createObjectURL(file);
setPendingFile(file);
setPreviewUrl(localUrl);
setMediaId(null);
setUploadError(null);
setUploading(true);
const csrfToken = buildCSRFHeaders()["X-CSRF-Token"] as string;
const result = await uploadFile(file, csrfToken);
setUploading(false);
if ("error" in result) {
setUploadError(result.error);
setPendingFile(null);
URL.revokeObjectURL(localUrl);
setPreviewUrl(null);
} else {
setMediaId(result.mediaId);
}
}
function removeAttachment() {
if (previewUrl) URL.revokeObjectURL(previewUrl);
setPendingFile(null);
setPreviewUrl(null);
setMediaId(null);
setUploadError(null);
}
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
e.preventDefault();
@@ -132,15 +186,80 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
rows={2}
maxLength={MAX + 1}
/>
{previewUrl && pendingFile && (
<div style={{ position: "relative", marginTop: "0.5rem", display: "inline-block" }}>
{/\.(mp4|mov)$/i.test(pendingFile.name) ? (
<video
src={previewUrl}
controls
style={{ maxWidth: "100%", maxHeight: "200px", borderRadius: "0.5rem", display: "block" }}
/>
) : (
<img
src={previewUrl}
alt="attachment preview"
style={{ maxWidth: "100%", maxHeight: "200px", borderRadius: "0.5rem", display: "block" }}
/>
)}
{uploading && (
<div style={{
position: "absolute", inset: 0, background: "rgba(0,0,0,0.4)",
borderRadius: "0.5rem", display: "flex", alignItems: "center", justifyContent: "center",
color: "#fff", fontSize: "0.75rem"
}}>
Uploading
</div>
)}
<button
type="button"
onClick={removeAttachment}
style={{
position: "absolute", top: "4px", right: "4px",
background: "rgba(0,0,0,0.6)", border: "none", borderRadius: "50%",
width: "20px", height: "20px", cursor: "pointer",
color: "#fff", fontSize: "12px", lineHeight: 1,
display: "flex", alignItems: "center", justifyContent: "center"
}}
title="Remove attachment"
>
×
</button>
</div>
)}
{uploadError && <p className="mx-compose-error">{uploadError}</p>}
{error && <p className="mx-compose-error">{error}</p>}
<div className="mx-compose-footer">
<span className="mx-compose-hint"> to post</span>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<button
type="button"
className="mx-action-btn"
title="Attach image or video"
onClick={() => fileInputRef.current?.click()}
disabled={uploading || mutation.isPending}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" />
</svg>
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*,video/mp4,video/quicktime"
style={{ display: "none" }}
onChange={handleFileChange}
/>
{uploading && (
<span style={{ fontSize: "0.75rem", color: "var(--mx-muted)" }}>
{pendingFile?.name}
</span>
)}
</div>
<div className="mx-compose-actions">
<CharCount current={text.length} max={MAX} />
<button
className="mx-btn-post"
onClick={submit}
disabled={!text.trim() || mutation.isPending}
disabled={!text.trim() || mutation.isPending || uploading}
>
{mutation.isPending ? "Posting…" : "Post"}
</button>
@@ -151,6 +270,31 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
);
}
function TweetMedia({ media }: { media: MediaItem[] }) {
const assetHost = getAssetHost();
return (
<div style={{ marginTop: "0.5rem", display: "flex", flexDirection: "column", gap: "0.5rem" }}>
{media.map((m) =>
/\.(mp4|mov)$/i.test(m.s3Key) ? (
<video
key={m.id}
src={`${assetHost}/${m.s3Key}`}
controls
style={{ maxWidth: "100%", borderRadius: "0.5rem" }}
/>
) : (
<img
key={m.id}
src={`${assetHost}/${m.s3Key}`}
alt=""
style={{ maxWidth: "100%", borderRadius: "0.5rem", display: "block" }}
/>
)
)}
</div>
);
}
function TweetCard({ tweet }: { tweet: Tweet }) {
const { userId: currentUserId } = useContext(AuthCtx);
const canModify = !!currentUserId && tweet.userId === currentUserId;
@@ -283,6 +427,10 @@ function TweetCard({ tweet }: { tweet: Tweet }) {
<p className="mx-tweet-text">{tweet.content}</p>
)}
{tweet.media && tweet.media.length > 0 && (
<TweetMedia media={tweet.media} />
)}
{error && !editing && <p className="mx-compose-error">{error}</p>}
</div>
</article>
@@ -294,7 +442,7 @@ function Feed() {
queryKey: ["tweets"],
queryFn: async () => {
const res = await readTweet({
fields: ["id", "content", "userId", "state"],
fields: ["id", "content", "userId", "state", { media: ["id", "s3Key"] }],
sort: "-id",
headers: buildCSRFHeaders(),
});

29
assets/js/upload.ts Normal file
View File

@@ -0,0 +1,29 @@
export interface UploadResult {
success: true;
mediaId: string;
url: string;
}
export interface UploadError {
success?: false;
error: string;
}
export async function uploadFile(
file: File,
csrfToken: string
): Promise<UploadResult | UploadError> {
const formData = new FormData();
formData.append("file", file);
// Do NOT set Content-Type — browser sets the multipart boundary automatically
const res = await fetch("/upload", {
method: "POST",
headers: { "X-CSRF-Token": csrfToken },
body: formData,
});
const json = await res.json();
if (!res.ok || !json.success) {
return { error: json.error ?? "Upload failed" };
}
return json as UploadResult;
}

View File

@@ -7,6 +7,14 @@
# General application configuration
import Config
config :waffle,
storage: Waffle.Storage.S3,
bucket: "mixer-bucket",
asset_host: "http://localhost:3900"
config :ex_aws,
json_codec: Jason
config :ash_typescript,
output_file: "assets/js/ash_rpc.ts",
run_endpoint: "/rpc/run",

View File

@@ -91,3 +91,16 @@ config :phoenix_live_view,
# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false
# Local S3-compatible storage (MinIO or similar at localhost:3901)
# Adjust access_key_id / secret_access_key to match your local server's credentials
config :ex_aws,
access_key_id: "GKdea8f62997a90ffa664135d2",
secret_access_key: "dd2f1757661a9e68cae6928a2fc950a2b2fd03b229d71038c98d4713b40ebba2",
region: "garage"
config :ex_aws, :s3,
scheme: "http://",
host: "localhost",
port: 3900,
region: "garage"

View File

@@ -9,6 +9,7 @@ defmodule Mixer.Posts do
resources do
resource Mixer.Posts.Tweet
resource Mixer.Posts.Media
end
typescript_rpc do
@@ -18,5 +19,9 @@ defmodule Mixer.Posts do
rpc_action :update_tweet, :update
rpc_action :destroy_tweet, :destroy
end
resource Mixer.Posts.Media do
rpc_action :read_media, :read
end
end
end

81
lib/mixer/posts/media.ex Normal file
View File

@@ -0,0 +1,81 @@
defmodule Mixer.Posts.Media do
use Ash.Resource,
otp_app: :mixer,
domain: Mixer.Posts,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [
AshTypescript.Resource
]
postgres do
table "media"
repo Mixer.Repo
end
typescript do
type_name "media"
end
actions do
defaults [:read]
create :upload do
accept [:s3_key]
change relate_actor(:user)
end
update :link_to_tweet do
accept [:tweet_id]
end
destroy :destroy do
primary? true
end
end
attributes do
uuid_primary_key :id
attribute :s3_key, :string do
allow_nil? false
public? true
end
attribute :user_id, :uuid do
allow_nil? false
public? true
end
end
relationships do
belongs_to :user, Mixer.Accounts.User do
attribute_writable? true
allow_nil? false
public? true
end
belongs_to :tweet, Mixer.Posts.Tweet do
allow_nil? true
public? true
end
end
policies do
policy action_type(:read) do
authorize_if always()
end
policy action(:upload) do
authorize_if actor_present()
end
policy action(:link_to_tweet) do
authorize_if relates_to_actor_via(:user)
end
policy action_type(:destroy) do
authorize_if relates_to_actor_via(:user)
end
end
end

View File

@@ -0,0 +1,24 @@
defmodule Mixer.Posts.MediaUploader do
use Waffle.Definition
@async false
@versions [:original]
@extensions ~w(.jpg .jpeg .png .gif .webp .mp4 .mov)
def validate({file, _scope}) do
ext = file.file_name |> Path.extname() |> String.downcase()
if ext in @extensions, do: :ok, else: {:error, "unsupported file type #{ext}"}
end
def storage_dir(_version, {_file, scope}), do: "uploads/media/#{scope.id}"
def filename(_version, {file, _scope}) do
Path.basename(file.file_name)
end
def s3_object_headers(_version, {file, _scope}) do
[content_type: MIME.from_path(file.file_name)]
end
def acl(_version, _), do: :public_read
end

View File

@@ -30,8 +30,25 @@ defmodule Mixer.Posts.Tweet do
create :create do
upsert? true
accept [:content]
argument :media_id, :uuid, allow_nil?: true
change relate_actor(:user)
change transition_state(:posted)
change fn changeset, context ->
case Ash.Changeset.get_argument(changeset, :media_id) do
nil ->
changeset
media_id ->
Ash.Changeset.after_action(changeset, fn _changeset, tweet ->
Mixer.Posts.Media
|> Ash.get!(media_id, authorize?: false)
|> Ash.Changeset.for_update(:link_to_tweet, %{tweet_id: tweet.id}, actor: context.actor)
|> Ash.update!()
{:ok, tweet}
end)
end
end
end
end
@@ -43,6 +60,12 @@ defmodule Mixer.Posts.Tweet do
public? true
end
attribute :likes, :integer do
allow_nil? false
default 0
public? true
end
attribute :user_id, :uuid do
allow_nil? false
public? true
@@ -56,6 +79,10 @@ defmodule Mixer.Posts.Tweet do
allow_nil? false
public? true
end
has_many :media, Mixer.Posts.Media do
public? true
end
end
policies do

View File

@@ -0,0 +1,47 @@
defmodule MixerWeb.UploadController do
use MixerWeb, :controller
alias Mixer.Posts.MediaUploader
def create(conn, %{"file" => %Plug.Upload{} = upload}) do
actor = conn.assigns[:current_user]
unless actor do
conn
|> put_status(:unauthorized)
|> json(%{error: "authentication required"})
else
scope = %{id: Ash.UUID.generate()}
case MediaUploader.store({upload, scope}) do
{:ok, file_name} ->
s3_key = "uploads/media/#{scope.id}/#{file_name}"
url = MediaUploader.url({file_name, scope})
Mixer.Posts.Media
|> Ash.Changeset.for_create(:upload, %{s3_key: s3_key}, actor: actor)
|> Ash.create()
|> case do
{:ok, media} ->
json(conn, %{success: true, mediaId: media.id, url: url})
{:error, error} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{success: false, error: inspect(error)})
end
{:error, reason} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{success: false, error: reason})
end
end
end
def create(conn, _params) do
conn
|> put_status(:bad_request)
|> json(%{error: "no file provided"})
end
end

View File

@@ -56,7 +56,8 @@ defmodule MixerWeb.Endpoint do
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser, AshJsonApi.Plug.Parser],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
json_decoder: Phoenix.json_library(),
length: 10_000_000
plug Plug.MethodOverride
plug Plug.Head

View File

@@ -41,6 +41,7 @@ defmodule MixerWeb.Router do
get "/feed", PageController, :index
post "/rpc/run", AshTypescriptRpcController, :run
post "/rpc/validate", AshTypescriptRpcController, :validate
post "/upload", UploadController, :create
auth_routes AuthController, Mixer.Accounts.User, path: "/auth"
sign_out_route AuthController

View File

@@ -0,0 +1,59 @@
defmodule Mixer.Media do
use Waffle.Definition
# Include ecto support (requires package waffle_ecto installed):
# use Waffle.Ecto.Definition
@versions [:original]
# To add a thumbnail version:
# @versions [:original, :thumb]
# Override the bucket on a per definition basis:
# def bucket do
# :custom_bucket_name
# end
# def bucket({_file, scope}) do
# scope.bucket || bucket()
# end
# Whitelist file extensions:
# def validate({file, _}) do
# file_extension = file.file_name |> Path.extname() |> String.downcase()
#
# case Enum.member?(~w(.jpg .jpeg .gif .png), file_extension) do
# true -> :ok
# false -> {:error, "invalid file type"}
# end
# end
# Define a thumbnail transformation:
# def transform(:thumb, _) do
# {:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
# end
# Override the persisted filenames:
# def filename(version, _) do
# version
# end
# Override the storage directory:
# def storage_dir(version, {file, scope}) do
# "uploads/user/avatars/#{scope.id}"
# end
# Provide a default URL if there hasn't been a file uploaded
# def default_url(version, scope) do
# "/images/avatars/default_#{version}.png"
# end
# Specify custom headers for s3 objects
# Available options are [:cache_control, :content_disposition,
# :content_encoding, :content_length, :content_type,
# :expect, :expires, :storage_class, :website_redirect_location]
#
# def s3_object_headers(version, {file, scope}) do
# [content_type: MIME.from_path(file.file_name)]
# end
end

View File

@@ -85,7 +85,13 @@ defmodule Mixer.MixProject do
{:gettext, "~> 1.0"},
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.2.0"},
{:bandit, "~> 1.5"}
{:bandit, "~> 1.5"},
{:waffle, "~> 1.1.10"},
{:waffle_ecto, "~> 0.0"},
{:ex_aws, "~> 2.1.2"},
{:ex_aws_s3, "~> 2.0"},
{:hackney, "~> 1.9"},
{:sweet_xml, "~> 0.6"}
]
end

View File

@@ -19,6 +19,7 @@
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
"castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"cinder": {:hex, :cinder, "0.12.1", "02ae4988e025fb32c37e4e7f2e491586b952918c0dd99d856da13271cd680e16", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.3", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "a48b5677c1f57619d9d7564fb2bd7928f93750a2e8c0b1b145852a30ecf2aa20"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"},
@@ -32,12 +33,15 @@
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
"ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"},
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.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.4", [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.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
@@ -50,13 +54,16 @@
"langchain": {:hex, :langchain, "0.6.3", "88794ef059c97521279996571ac77daf15f8ccf7c6ccad2716d969cbb052d6ca", [:mix], [{:abacus, "~> 2.1.0", [hex: :abacus, repo: "hexpm", optional: true]}, {:dotenvy, "~> 1.1", [hex: :dotenvy, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26.2 or ~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: true]}, {:nx, ">= 0.7.0", [hex: :nx, repo: "hexpm", optional: true]}, {:req, ">= 0.5.3", [hex: :req, repo: "hexpm", optional: false]}, {:req_llm, "~> 1.6", [hex: :req_llm, repo: "hexpm", optional: true]}], "hexpm", "9f0eeac704bc1b01fb91e0ae38559f9e643b2e067c8e98c43ba06f2213aa14f4"},
"lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"open_api_spex": {:hex, :open_api_spex, "3.22.2", "0b3c4f572ee69cb6c936abf426b9d84d8eebd34960871fd77aead746f0d69cb0", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {: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 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "0a4fc08472d75e9cfe96e0748c6b1565b3b4398f97bf43fcce41b41b6fd3fb33"},
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
@@ -79,7 +86,9 @@
"spark": {:hex, :spark, "2.6.1", "b0100216d3883c6a281cb2434af45afbd808695aadb034923cbaf7d8a2ba46ab", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "77bbefa5263bb6b70e1195bc0fc662ddb8ef5937a356a77ae072e56983ad13f0"},
"spitfire": {:hex, :spitfire, "0.3.10", "19aea9914132456515e8f7d592f63ab9f3130876b0252e834d2390bdd8becb24", [:mix], [], "hexpm", "6a6a5f77eb4165249c76199cd2d01fb595bac9207aed3de551918ac1c2bc9267"},
"splode": {:hex, :splode, "0.3.0", "ff8effecc509a51245df2f864ec78d849248647c37a75886033e3b1a53ca9470", [:mix], [], "hexpm", "73cfd0892d7316d6f2c93e6e8784bd6e137b2aa38443de52fd0a25171d106d81"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"swoosh": {:hex, :swoosh, "1.24.0", "4df9645aeeef925a2eb10f7a588a6a09ddd6d370c5dfbd3e821b699c574bdf57", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6ddd84550800468d0e2c15a8aaff924a64c014ed6cff90318077efd1672b8b3b"},
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
@@ -89,6 +98,8 @@
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
"usage_rules": {:hex, :usage_rules, "1.2.5", "3737b44ecba9fa816e04abadf5199bc78b68f05d49ae79f1fa785342167a721f", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "509eb67b7a7f90514889b602c41692b45956dc3e381ca0e25505dccd8de90390"},
"waffle": {:hex, :waffle, "1.1.10", "0f847ed6f95349af258a90f0f70ffea02b3d3729c4eb78f6fae7bf776e91779e", [:mix], [{:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.1", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "859ba6377b78f0a51bc9596227b194f26241efbbd408bd217450c22b0f359cc4"},
"waffle_ecto": {:hex, :waffle_ecto, "0.0.12", "e5c17c49b071b903df71861c642093281123142dc4e9908c930db3e06795b040", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:waffle, "~> 1.0", [hex: :waffle, repo: "hexpm", optional: false]}], "hexpm", "585fe6371057066d2e8e3383ddd7a2437ff0668caf3f4cbf5a041e0de9837168"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
"xema": {:hex, :xema, "0.17.7", "7eeda174b70a5f7fb1cc2e9fa3a7d4e78e206a99866c107d477309410b678cf2", [:mix], [{:conv_case, "~> 0.2.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "7e3d7c0629282c21af6aaa5e2ba593218cd764a57bd1ae49e2c4412324e904cd"},

View File

@@ -0,0 +1,38 @@
defmodule Mixer.Repo.Migrations.AddPostsMediaS3 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:tweets) do
add :likes, :bigint, null: false, default: 0
end
create table(:media, primary_key: false) do
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
add :s3_key, :text, null: false
add :tweet_id,
references(:tweets,
column: :id,
name: "media_tweet_id_fkey",
type: :uuid,
prefix: "public"
), null: false
end
end
def down do
drop constraint(:media, "media_tweet_id_fkey")
drop table(:media)
alter table(:tweets) do
remove :likes
end
end
end

View File

@@ -0,0 +1,32 @@
defmodule Mixer.Repo.Migrations.AddUserIdToMediaAndAllowNullTweetId do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:media) do
modify :tweet_id, :uuid, null: true
add :user_id,
references(:users,
column: :id,
name: "media_user_id_fkey",
type: :uuid,
prefix: "public"
), null: false
end
end
def down do
drop constraint(:media, "media_user_id_fkey")
alter table(:media) do
remove :user_id
modify :tweet_id, :uuid, null: false
end
end
end

View File

@@ -0,0 +1,75 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "s3_key",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "media_tweet_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "tweets"
},
"scale": null,
"size": null,
"source": "tweet_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"create_table_options": null,
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "D707EF6F25A8BD5E01A333A9C8CBE9AAAF015CFB31BF4F5A4DE4CDA2CD9A4DD8",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mixer.Repo",
"schema": null,
"table": "media"
}

View File

@@ -0,0 +1,106 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "s3_key",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "media_user_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "users"
},
"scale": null,
"size": null,
"source": "user_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "media_tweet_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "tweets"
},
"scale": null,
"size": null,
"source": "tweet_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"create_table_options": null,
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "8E26F7D27DBFD094DC6AA9EDA77C4D40767CF9F3B738CF6F31839A68AD62A886",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mixer.Repo",
"schema": null,
"table": "media"
}

View File

@@ -0,0 +1,99 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "content",
"type": "text"
},
{
"allow_nil?": false,
"default": "0",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "likes",
"type": "bigint"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "tweets_user_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "users"
},
"scale": null,
"size": null,
"source": "user_id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "\"drafted\"",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "state",
"type": "text"
}
],
"base_filter": null,
"check_constraints": [],
"create_table_options": null,
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "72C986AFFFB09E7394DC54328AC3F7E47C24599B894E1E65964132E2598AB55C",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mixer.Repo",
"schema": null,
"table": "tweets"
}