From 944b10f75c87ba5aa60f8645e6877006ba399186 Mon Sep 17 00:00:00 2001 From: Darwin D Wu Date: Thu, 9 Jul 2026 00:13:54 -0700 Subject: [PATCH 1/7] remove update capability for pastes don't see a need for it, and can be added back if there's an actual case to support it --- lib/textbin/pastes.ex | 6 --- .../controllers/apiv1/paste_controller.ex | 23 ----------- lib/textbin_web/router.ex | 2 +- test/textbin/pastes_test.exs | 15 -------- .../apiv1/paste_controller_test.exs | 38 ++----------------- 5 files changed, 5 insertions(+), 79 deletions(-) diff --git a/lib/textbin/pastes.ex b/lib/textbin/pastes.ex index e9520d7..eaf9fce 100644 --- a/lib/textbin/pastes.ex +++ b/lib/textbin/pastes.ex @@ -20,12 +20,6 @@ defmodule Textbin.Pastes do |> Repo.insert() end - def update_paste(%Paste{} = paste, attrs) do - paste - |> Paste.changeset(attrs) - |> Repo.update() - end - def delete_paste(%Paste{} = paste) do Repo.delete(paste) end diff --git a/lib/textbin_web/controllers/apiv1/paste_controller.ex b/lib/textbin_web/controllers/apiv1/paste_controller.ex index 7752ba8..6f56251 100644 --- a/lib/textbin_web/controllers/apiv1/paste_controller.ex +++ b/lib/textbin_web/controllers/apiv1/paste_controller.ex @@ -121,29 +121,6 @@ defmodule TextbinWeb.ApiV1.PasteController do render(conn, :show, paste: paste) end - def update(conn, %{"id" => id} = params) do - paste = Pastes.get_paste!(id) - paste_params = paste_params(params) - - case validate_paste_size(paste_params) do - :ok -> - case Pastes.update_paste(paste, paste_params) do - {:ok, paste} -> - render(conn, :show, paste: paste) - - {:error, changeset} -> - render_changeset_errors(conn, changeset) - end - - {:error, :too_large} -> - render_too_large(conn) - end - end - - defp paste_params(%{"data" => data}) when is_binary(data), do: %{"data" => data} - defp paste_params(%{"paste" => %{"data" => data}}) when is_binary(data), do: %{"data" => data} - defp paste_params(_params), do: %{"data" => nil} - def delete(conn, %{"id" => id}) do paste = Pastes.get_paste!(id) {:ok, _paste} = Pastes.delete_paste(paste) diff --git a/lib/textbin_web/router.ex b/lib/textbin_web/router.ex index c8d1f0c..020d575 100644 --- a/lib/textbin_web/router.ex +++ b/lib/textbin_web/router.ex @@ -31,7 +31,7 @@ defmodule TextbinWeb.Router do scope "/api/v1", TextbinWeb.ApiV1 do pipe_through :api - resources "/pastes", PasteController, except: [:new, :edit] + resources "/pastes", PasteController, except: [:new, :edit, :update] end # Enable LiveDashboard and Swoosh mailbox preview in development diff --git a/test/textbin/pastes_test.exs b/test/textbin/pastes_test.exs index 5b5f48b..aef8074 100644 --- a/test/textbin/pastes_test.exs +++ b/test/textbin/pastes_test.exs @@ -6,7 +6,6 @@ defmodule Textbin.PastesTest do describe "pastes" do @valid_attrs %{data: "some data"} - @update_attrs %{data: "updated data"} @invalid_attrs %{data: nil} test "list_pastes/0 returns all pastes" do @@ -36,20 +35,6 @@ defmodule Textbin.PastesTest do assert {:error, %Ecto.Changeset{}} = Pastes.create_paste(@invalid_attrs) end - test "update_paste/2 with valid data updates the paste" do - {:ok, paste} = Pastes.create_paste(@valid_attrs) - - assert {:ok, %Paste{} = paste} = Pastes.update_paste(paste, @update_attrs) - assert paste.data == "updated data" - end - - test "update_paste/2 with invalid data returns error changeset" do - {:ok, paste} = Pastes.create_paste(@valid_attrs) - - assert {:error, %Ecto.Changeset{}} = Pastes.update_paste(paste, @invalid_attrs) - assert Pastes.get_paste!(paste.id).data == paste.data - end - test "delete_paste/1 deletes the paste" do {:ok, paste} = Pastes.create_paste(@valid_attrs) diff --git a/test/textbin_web/controllers/apiv1/paste_controller_test.exs b/test/textbin_web/controllers/apiv1/paste_controller_test.exs index d0333f6..9cf0284 100644 --- a/test/textbin_web/controllers/apiv1/paste_controller_test.exs +++ b/test/textbin_web/controllers/apiv1/paste_controller_test.exs @@ -5,7 +5,6 @@ defmodule TextbinWeb.ApiV1.PasteControllerTest do @max_paste_bytes 1_048_576 @create_attrs %{data: "some data"} - @update_attrs %{data: "updated data"} @invalid_attrs %{data: nil} describe "index" do @@ -118,42 +117,13 @@ defmodule TextbinWeb.ApiV1.PasteControllerTest do end describe "update paste" do - test "renders paste when data is valid", %{conn: conn} do - {:ok, paste} = Pastes.create_paste(%{data: "some data"}) - - conn = patch(conn, ~p"/api/v1/pastes/#{paste.id}", paste: @update_attrs) - - assert %{"id" => id} = json_response(conn, 200)["data"] - assert_stored_data(id, "updated data") - - conn = get(conn, ~p"/api/v1/pastes/#{id}") - - assert %{ - "id" => ^id - } = response_data = json_response(conn, 200)["data"] - - refute Map.has_key?(response_data, "data") - end - - test "renders errors when data is invalid", %{conn: conn} do + test "is not routable", %{conn: conn} do {:ok, paste} = Pastes.create_paste(%{data: "some data"}) - conn = patch(conn, ~p"/api/v1/pastes/#{paste.id}", paste: @invalid_attrs) - - assert %{"data" => [_]} = json_response(conn, 422)["errors"] - end - - test "rejects data over the configured size limit", %{conn: conn} do - {:ok, paste} = Pastes.create_paste(%{data: "some data"}) - too_large_data = String.duplicate("a", @max_paste_bytes + 1) + conn = patch(conn, ~p"/api/v1/pastes/#{paste.id}", paste: %{data: "updated data"}) - conn = patch(conn, ~p"/api/v1/pastes/#{paste.id}", %{data: too_large_data}) - - assert %{ - "errors" => %{ - "detail" => "Paste data exceeds the maximum size of 1048576 bytes" - } - } = json_response(conn, 413) + assert response(conn, 404) + assert_stored_data(paste.id, "some data") end end From 373341245b44a079a86f451e624edd796903d6f2 Mon Sep 17 00:00:00 2001 From: Darwin D Wu Date: Thu, 9 Jul 2026 00:18:49 -0700 Subject: [PATCH 2/7] allow deleting paste from UI --- lib/textbin_web/live/ui/paste_live.ex | 17 ++++++++++++ .../live/ui/paste_live/detail.html.heex | 17 +++++++++--- .../live/ui/paste_live/index.html.heex | 24 ++++++++++++----- test/textbin_web/live/ui/paste_live_test.exs | 26 +++++++++++++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/lib/textbin_web/live/ui/paste_live.ex b/lib/textbin_web/live/ui/paste_live.ex index f707692..a708036 100644 --- a/lib/textbin_web/live/ui/paste_live.ex +++ b/lib/textbin_web/live/ui/paste_live.ex @@ -15,6 +15,23 @@ defmodule TextbinWeb.UI.PasteLive do {:noreply, assign(socket, :paste, Pastes.get_paste!(id))} end + def handle_event("delete", %{"id" => id}, %{assigns: %{live_action: :index}} = socket) do + paste = Pastes.get_paste!(id) + {:ok, _paste} = Pastes.delete_paste(paste) + + {:noreply, stream_delete(socket, :pastes, paste)} + end + + def handle_event("delete", %{"id" => id}, %{assigns: %{live_action: :show}} = socket) do + paste = Pastes.get_paste!(id) + {:ok, _paste} = Pastes.delete_paste(paste) + + {:noreply, + socket + |> put_flash(:info, "Paste deleted") + |> push_navigate(to: ~p"/pastes")} + end + def render(%{live_action: :show} = assigns), do: detail(assigns) def render(assigns), do: index(assigns) end diff --git a/lib/textbin_web/live/ui/paste_live/detail.html.heex b/lib/textbin_web/live/ui/paste_live/detail.html.heex index 97a6b76..9f0f4ef 100644 --- a/lib/textbin_web/live/ui/paste_live/detail.html.heex +++ b/lib/textbin_web/live/ui/paste_live/detail.html.heex @@ -15,9 +15,20 @@ {Calendar.strftime(@paste.inserted_at, "%Y-%m-%d %H:%M:%S UTC")} - <.link navigate={~p"/pastes"} class="btn btn-sm btn-ghost"> - Back to pastes - +
+ <.link navigate={~p"/pastes"} class="btn btn-sm btn-ghost"> + Back to pastes + + +
             {paste.id}
           
-          
+          
+ + +
diff --git a/test/textbin_web/live/ui/paste_live_test.exs b/test/textbin_web/live/ui/paste_live_test.exs index 99e4f76..e06154a 100644 --- a/test/textbin_web/live/ui/paste_live_test.exs +++ b/test/textbin_web/live/ui/paste_live_test.exs @@ -33,5 +33,31 @@ defmodule TextbinWeb.UI.PasteLiveTest do assert has_element?(view, "a[href='/pastes']", "Back to pastes") end + test "deletes a paste from the list", %{conn: conn} do + {:ok, paste} = Pastes.create_paste(%{data: "delete from list"}) + + {:ok, view, _html} = live(conn, ~p"/pastes") + + view + |> element("#delete-paste-#{paste.id}") + |> render_click() + + refute has_element?(view, "##{stream_id(paste)}") + assert_raise Ecto.NoResultsError, fn -> Pastes.get_paste!(paste.id) end + end + + test "deletes a paste from the detail page", %{conn: conn} do + {:ok, paste} = Pastes.create_paste(%{data: "delete from detail"}) + + {:ok, view, _html} = live(conn, ~p"/pastes/#{paste.id}") + + view + |> element("#delete-paste-#{paste.id}") + |> render_click() + + assert_redirect(view, ~p"/pastes") + assert_raise Ecto.NoResultsError, fn -> Pastes.get_paste!(paste.id) end + end + defp stream_id(paste), do: "pastes-#{paste.id}" end From 2f916d8e42638ce7f572f0c04a93bad9645fbcd9 Mon Sep 17 00:00:00 2001 From: Darwin D Wu Date: Thu, 9 Jul 2026 00:24:24 -0700 Subject: [PATCH 3/7] add confirmation before deletion --- lib/textbin_web/live/ui/paste_live/detail.html.heex | 1 + lib/textbin_web/live/ui/paste_live/index.html.heex | 1 + test/textbin_web/live/ui/paste_live_test.exs | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/lib/textbin_web/live/ui/paste_live/detail.html.heex b/lib/textbin_web/live/ui/paste_live/detail.html.heex index 9f0f4ef..ac5ed36 100644 --- a/lib/textbin_web/live/ui/paste_live/detail.html.heex +++ b/lib/textbin_web/live/ui/paste_live/detail.html.heex @@ -24,6 +24,7 @@ type="button" phx-click="delete" phx-value-id={@paste.id} + data-confirm="Delete this paste?" class="btn btn-sm btn-error" > <.icon name="hero-trash" class="size-4" /> Delete diff --git a/lib/textbin_web/live/ui/paste_live/index.html.heex b/lib/textbin_web/live/ui/paste_live/index.html.heex index 59d07ae..3901b85 100644 --- a/lib/textbin_web/live/ui/paste_live/index.html.heex +++ b/lib/textbin_web/live/ui/paste_live/index.html.heex @@ -51,6 +51,7 @@ type="button" phx-click="delete" phx-value-id={paste.id} + data-confirm="Delete this paste?" class="btn btn-ghost btn-xs text-error hover:bg-error/10" aria-label="Delete paste" > diff --git a/test/textbin_web/live/ui/paste_live_test.exs b/test/textbin_web/live/ui/paste_live_test.exs index e06154a..b33b147 100644 --- a/test/textbin_web/live/ui/paste_live_test.exs +++ b/test/textbin_web/live/ui/paste_live_test.exs @@ -38,6 +38,11 @@ defmodule TextbinWeb.UI.PasteLiveTest do {:ok, view, _html} = live(conn, ~p"/pastes") + assert has_element?( + view, + "#delete-paste-#{paste.id}[data-confirm='Delete this paste?']" + ) + view |> element("#delete-paste-#{paste.id}") |> render_click() @@ -51,6 +56,11 @@ defmodule TextbinWeb.UI.PasteLiveTest do {:ok, view, _html} = live(conn, ~p"/pastes/#{paste.id}") + assert has_element?( + view, + "#delete-paste-#{paste.id}[data-confirm='Delete this paste?']" + ) + view |> element("#delete-paste-#{paste.id}") |> render_click() From 2cc755682364eecc44287a97e5d25cbe2b9cc7ef Mon Sep 17 00:00:00 2001 From: Darwin D Wu Date: Thu, 9 Jul 2026 00:26:52 -0700 Subject: [PATCH 4/7] initialize git-cliff config --- cliff.toml | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 cliff.toml diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..a18fc20 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,94 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + + +[changelog] +# A Tera template to be rendered for each release in the changelog. +# See https://keats.github.io/tera/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %} +""" +# Remove leading and trailing whitespaces from the changelog's body. +trim = true +# Render body even when there are no releases to process. +render_always = true +# An array of regex based postprocessors to modify the changelog. +postprocessors = [ + # Replace the placeholder with a URL. + #{ pattern = '', replace = "https://github.com/orhun/git-cliff" }, +] +# render body even when there are no releases to process +# render_always = true +# output file path +# output = "test.md" + +[git] +# Parse commits according to the conventional commits specification. +# See https://www.conventionalcommits.org +conventional_commits = true +# Exclude commits that do not match the conventional commits specification. +filter_unconventional = true +# Require all commits to be conventional. +# Takes precedence over filter_unconventional. +require_conventional = false +# Split commits on newlines, treating each line as an individual commit. +split_commits = false +# An array of regex based parsers to modify commit messages prior to further processing. +commit_preprocessors = [ + # Replace issue numbers with link templates to be updated in `changelog.postprocessors`. + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit message using https://github.com/crate-ci/typos. + # If the spelling is incorrect, it will be fixed automatically. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# Prevent commits that are breaking from being excluded by commit parsers. +protect_breaking_commits = false +# An array of regex based parsers for extracting data from the commit message. +# Assigns commits to groups. +# Optionally sets the commit's scope and can decide to exclude commits from further processing. +commit_parsers = [ + { message = "^feat", group = "๐Ÿš€ Features" }, + { message = "^fix", group = "๐Ÿ› Bug Fixes" }, + { message = "^doc", group = "๐Ÿ“š Documentation" }, + { message = "^perf", group = "โšก Performance" }, + { message = "^refactor", group = "๐Ÿšœ Refactor" }, + { message = "^style", group = "๐ŸŽจ Styling" }, + { message = "^test", group = "๐Ÿงช Testing" }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|^ci", group = "โš™๏ธ Miscellaneous Tasks" }, + { body = ".*security", group = "๐Ÿ›ก๏ธ Security" }, + { message = "^revert", group = "โ—€๏ธ Revert" }, + { message = ".*", group = "๐Ÿ’ผ Other" }, +] +# Exclude commits that are not matched by any commit parser. +filter_commits = false +# Fail on a commit that is not matched by any commit parser. +fail_on_unmatched_commit = false +# An array of link parsers for extracting external references, and turning them into URLs, using regex. +link_parsers = [] +# Include only the tags that belong to the current branch. +use_branch_tags = false +# Order releases topologically instead of chronologically. +topo_order = false +# Order commits topologically instead of chronologically. +topo_order_commits = true +# Order of commits in each group/release within the changelog. +# Allowed values: newest, oldest +sort_commits = "oldest" +# Process submodules commits +recurse_submodules = false From f934dcd0c981506c68488e96e1745cd385fc26d6 Mon Sep 17 00:00:00 2001 From: Darwin D Wu Date: Thu, 9 Jul 2026 00:27:31 -0700 Subject: [PATCH 5/7] remove no longer needed golang deps --- flake.nix | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/flake.nix b/flake.nix index ee01003..3192177 100644 --- a/flake.nix +++ b/flake.nix @@ -30,19 +30,9 @@ beam.packages.erlang_28.rebar3 erlang_28 - # Golang - go - golangci-lint - gotests - gomodifytags - gore - gotools - delve - # LSPs beamPackages.expert erlang-language-platform - gopls # Tools watchman @@ -52,7 +42,6 @@ shellcheck git-cliff yaml-language-server - goreleaser postgresql ]; From 7672c4598206bb30b65123e98a4f5b8d3270d6bf Mon Sep 17 00:00:00 2001 From: Darwin D Wu Date: Thu, 9 Jul 2026 00:29:18 -0700 Subject: [PATCH 6/7] add CI job for conventional commits --- .github/workflows/commits.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/commits.yml diff --git a/.github/workflows/commits.yml b/.github/workflows/commits.yml new file mode 100644 index 0000000..a85e552 --- /dev/null +++ b/.github/workflows/commits.yml @@ -0,0 +1,21 @@ +--- +name: Conventional Commits + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +jobs: + validate-pr-title: + name: Validate title + runs-on: blacksmith-2vcpu-ubuntu-2404 + steps: + - name: PR Conventional Commit Validation + uses: ytanikin/pr-conventional-commits@1.5.2 + with: + task_types: >- + ["feat","fix","doc","docs","test","ci","refactor","perf","chore","revert","style","security"] From 6e4c4b008fa31b01c622d7d69b2f83f8a4c9c089 Mon Sep 17 00:00:00 2001 From: Darwin D Wu Date: Thu, 9 Jul 2026 00:34:20 -0700 Subject: [PATCH 7/7] bump the otp version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6a1cf8..8f2d282 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ env: DATABASE_PORT: 5432 DATABASE_TEST_NAME: textbin_test - OTP_VERSION: "28.1" + OTP_VERSION: "28.5" ELIXIR_VERSION: "1.20" jobs: