From e0afe65a1be777a42fa822518aa8d95e4c03f36b Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Tue, 10 Jun 2025 11:04:07 +0200 Subject: [PATCH 01/10] fix docker image creation and document it --- README.md | 8 ++++++++ bruno/CodeSnippets/delete_snippet.bru | 4 ++-- src/snippets/main.clj | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f6bbde3..093d70c 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,11 @@ App to store my code snippets. ``` $ clojure -M -m snippets.infra.api ``` + +### How to create docker image + +At this point I'm using a private AWS ECR repository for the project and deploying it in my homelab. + +```shell +$ docker buildx build --platform linux/amd64,linux/arm64 -t 853019563312.dkr.ecr.eu-central-1.amazonaws.com/code-snippets:latest --push . +``` diff --git a/bruno/CodeSnippets/delete_snippet.bru b/bruno/CodeSnippets/delete_snippet.bru index 07ef77e..8e9a782 100644 --- a/bruno/CodeSnippets/delete_snippet.bru +++ b/bruno/CodeSnippets/delete_snippet.bru @@ -5,13 +5,13 @@ meta { } delete { - url: {{host}}/api/snippet?id=90d00a80-78c4-4bc9-a066-01c988599d05 + url: {{host}}/api/snippet?id=d77d3463-c76e-4c53-a1d5-ecaf16c6c54e body: none auth: none } params:query { - id: 90d00a80-78c4-4bc9-a066-01c988599d05 + id: d77d3463-c76e-4c53-a1d5-ecaf16c6c54e } body:json { diff --git a/src/snippets/main.clj b/src/snippets/main.clj index d6fef1e..4e0e10d 100644 --- a/src/snippets/main.clj +++ b/src/snippets/main.clj @@ -1,5 +1,5 @@ (ns snippets.main - (:require [snippets.api :as api]) + (:require [snippets.infra.api :as api]) (:gen-class)) (defn -main [] From ee1c6a50e94f7f88475fe4a2d9800632b8653530 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Tue, 10 Jun 2025 15:13:29 +0200 Subject: [PATCH 02/10] fix edit snippet use case --- src/snippets/use_cases/edit.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snippets/use_cases/edit.clj b/src/snippets/use_cases/edit.clj index 5db3816..8e9ac2b 100644 --- a/src/snippets/use_cases/edit.clj +++ b/src/snippets/use_cases/edit.clj @@ -9,7 +9,7 @@ [:map {:closed true} [:markdown {:optional true} :string] [:title {:optional true} :string] - [:tags [:seqable :string]] + [:tags {:optional true} [:seqable :string]] [:slug {:optional true} :string]])) (defn edit-snippet [id patch] From a2352d19c267f01cb6ba680f4cedb95f686ff4db Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Mon, 16 Jun 2025 11:34:21 +0200 Subject: [PATCH 03/10] include tags in view-snippet --- src/snippets/infra/db.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snippets/infra/db.clj b/src/snippets/infra/db.clj index d35eed5..1273cf5 100644 --- a/src/snippets/infra/db.clj +++ b/src/snippets/infra/db.clj @@ -22,7 +22,7 @@ (format "(quote (-> (from :snippets [title pub-date tags slug markdown {:xt/id id}]) (order-by {:val pub-date, :dir :desc, :nulls :last}) (offset %s) (limit %s)))" skip limit))))) (defn get-snippet-by-id [snippet-id] - (first (xt/q client ['#(from :snippets [{:xt/id %} slug title {:xt/id id} markdown pub-date]) snippet-id]))) + (first (xt/q client ['#(from :snippets [{:xt/id %} slug title tags {:xt/id id} markdown pub-date]) snippet-id]))) (defn put-snippet [id snippet] (t/log! {:level :info, :data {:snippet snippet :id id}} "Saving new snippet to db") From bbeb1b6ba0d1fa6c27c1266b716a6db027933fcd Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Mon, 16 Jun 2025 12:24:25 +0200 Subject: [PATCH 04/10] init view / list tags --- bruno/CodeSnippets/edit_snippet.bru | 4 ++-- bruno/CodeSnippets/get_snippets.bru | 4 ++-- bruno/CodeSnippets/get_tags.bru | 19 +++++++++++++++++++ src/snippets/infra/api.clj | 6 ++++++ src/snippets/infra/db.clj | 3 +++ src/snippets/use_cases/view.clj | 4 ++++ 6 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 bruno/CodeSnippets/get_tags.bru diff --git a/bruno/CodeSnippets/edit_snippet.bru b/bruno/CodeSnippets/edit_snippet.bru index fa6d897..dbfd39d 100644 --- a/bruno/CodeSnippets/edit_snippet.bru +++ b/bruno/CodeSnippets/edit_snippet.bru @@ -16,7 +16,7 @@ params:query { body:json { { - "title": "Updated from Bruno", - "tags": ["code", "mock"] + "title": "quick way to push last jj commit to git", + "tags": ["jj", "git"] } } diff --git a/bruno/CodeSnippets/get_snippets.bru b/bruno/CodeSnippets/get_snippets.bru index 8bf73c4..e2de66e 100644 --- a/bruno/CodeSnippets/get_snippets.bru +++ b/bruno/CodeSnippets/get_snippets.bru @@ -5,13 +5,13 @@ meta { } get { - url: {{host}}/api/snippets?limit=100&skip=0 + url: {{host}}/api/snippets?limit=25&skip=0 body: none auth: none } params:query { - limit: 100 + limit: 25 skip: 0 } diff --git a/bruno/CodeSnippets/get_tags.bru b/bruno/CodeSnippets/get_tags.bru new file mode 100644 index 0000000..a8a876b --- /dev/null +++ b/bruno/CodeSnippets/get_tags.bru @@ -0,0 +1,19 @@ +meta { + name: get_tags + type: http + seq: 8 +} + +get { + url: {{host}}/api/tags + body: none + auth: none +} + +body:json { + { + "title": "Test Snippet", + "markdown": "## Cool Snippet\ndoes a cool thing", + "tags": ["git", "jj"] + } +} diff --git a/src/snippets/infra/api.clj b/src/snippets/infra/api.clj index 1e96047..65e07f5 100644 --- a/src/snippets/infra/api.clj +++ b/src/snippets/infra/api.clj @@ -47,6 +47,11 @@ {:status 200 :body (format "Deleted snippet with id: %s if it existed" id)})) +(defn handle-view-tags [_args] + (let [tags (snippets.use-cases.view/view-tags)] + {:status 200 + :body tags})) + (defn wrap [handler id] (fn [request] (update (handler request) :wrap (fnil conj '()) id))) @@ -58,6 +63,7 @@ mm/wrap-format [wrap :api]]} ["/ping" {:get handle-ping}] + ["/tags" {:get handle-view-tags}] ["/snippets" {:get handle-view-snippets}] ["/snippet" {:post handle-create-snippet :get handle-view-snippet diff --git a/src/snippets/infra/db.clj b/src/snippets/infra/db.clj index 1273cf5..8e52d91 100644 --- a/src/snippets/infra/db.clj +++ b/src/snippets/infra/db.clj @@ -39,3 +39,6 @@ (defn patch-snippet [id patch] (t/log! {:level :info, :data {:patch patch :id id}} "Patching snippet") (xt/execute-tx client [[:patch-docs :snippets (merge {:xt/id id} patch)]])) + +(defn list-tags [] + (xt/q client '(-> (from :snippets [{:xt/id id} tags]) (unnest {:tag tags}) (without :tags) (aggregate tag {:ids (array-agg id)})))) diff --git a/src/snippets/use_cases/view.clj b/src/snippets/use_cases/view.clj index ac6e83b..2eeba6e 100644 --- a/src/snippets/use_cases/view.clj +++ b/src/snippets/use_cases/view.clj @@ -9,3 +9,7 @@ (defn view-snippets [& args] (db/list-snippets args)) + +(defn view-tags [] + (t/log! {:level :info} "Viewing tags") + (map #(assoc % :count (count (:ids %))) (db/list-tags))) From e11719b40f9fd06a6694dafc2efcb014b1ac53cb Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Mon, 16 Jun 2025 13:04:36 +0200 Subject: [PATCH 05/10] change list tags to only list tags --- src/snippets/infra/db.clj | 3 ++- src/snippets/use_cases/view.clj | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/snippets/infra/db.clj b/src/snippets/infra/db.clj index 8e52d91..537ce1b 100644 --- a/src/snippets/infra/db.clj +++ b/src/snippets/infra/db.clj @@ -41,4 +41,5 @@ (xt/execute-tx client [[:patch-docs :snippets (merge {:xt/id id} patch)]])) (defn list-tags [] - (xt/q client '(-> (from :snippets [{:xt/id id} tags]) (unnest {:tag tags}) (without :tags) (aggregate tag {:ids (array-agg id)})))) + ;; (xt/q client '(-> (from :snippets [{:xt/id id} tags]) (unnest {:tag tags}) (without :tags) (aggregate tag {:ids (array-agg id)})))) + (xt/q client '(-> (from :snippets [{:xt/id id} tags]) (unnest {:tag tags}) (without :tags) (aggregate tag {:count (count id)})))) diff --git a/src/snippets/use_cases/view.clj b/src/snippets/use_cases/view.clj index 2eeba6e..98b66a6 100644 --- a/src/snippets/use_cases/view.clj +++ b/src/snippets/use_cases/view.clj @@ -12,4 +12,4 @@ (defn view-tags [] (t/log! {:level :info} "Viewing tags") - (map #(assoc % :count (count (:ids %))) (db/list-tags))) + (db/list-tags)) From 6bbdb9c348a907efeffba4ec2c0a68c20bd026d0 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Mon, 16 Jun 2025 15:42:52 +0200 Subject: [PATCH 06/10] add get by slug and tag --- bruno/CodeSnippets/get_snippet_by_slug.bru | 23 ++++++++++++++++++++++ bruno/CodeSnippets/get_tag.bru | 23 ++++++++++++++++++++++ src/snippets/infra/api.clj | 17 ++++++++++++++++ src/snippets/infra/db.clj | 8 ++++++++ src/snippets/use_cases/view.clj | 8 ++++++++ 5 files changed, 79 insertions(+) create mode 100644 bruno/CodeSnippets/get_snippet_by_slug.bru create mode 100644 bruno/CodeSnippets/get_tag.bru diff --git a/bruno/CodeSnippets/get_snippet_by_slug.bru b/bruno/CodeSnippets/get_snippet_by_slug.bru new file mode 100644 index 0000000..3c17768 --- /dev/null +++ b/bruno/CodeSnippets/get_snippet_by_slug.bru @@ -0,0 +1,23 @@ +meta { + name: get_snippet_by_slug + type: http + seq: 10 +} + +get { + url: {{host}}/api/snippet-by-slug?slug=netcat-over-ping + body: none + auth: none +} + +params:query { + slug: netcat-over-ping +} + +body:json { + { + "title": "Test Snippet", + "markdown": "## Cool Snippet\ndoes a cool thing", + "tags": ["git", "jj"] + } +} diff --git a/bruno/CodeSnippets/get_tag.bru b/bruno/CodeSnippets/get_tag.bru new file mode 100644 index 0000000..c040ed9 --- /dev/null +++ b/bruno/CodeSnippets/get_tag.bru @@ -0,0 +1,23 @@ +meta { + name: get_tag + type: http + seq: 9 +} + +get { + url: {{host}}/api/tag?tag=git + body: none + auth: none +} + +params:query { + tag: git +} + +body:json { + { + "title": "Test Snippet", + "markdown": "## Cool Snippet\ndoes a cool thing", + "tags": ["git", "jj"] + } +} diff --git a/src/snippets/infra/api.clj b/src/snippets/infra/api.clj index 65e07f5..5f2c6fe 100644 --- a/src/snippets/infra/api.clj +++ b/src/snippets/infra/api.clj @@ -52,6 +52,21 @@ {:status 200 :body tags})) +(defn handle-view-snippets-by-tag [{params :query-params}] + (let [tag (get params "tag")] + {:status 200 + :body (snippets.use-cases.view/view-snippets-by-tag tag)})) + +(defn handle-view-tags [_] + (let [tags (snippets.use-cases.view/view-tags)] + {:status 200 + :body tags})) + +(defn handle-view-snippet-by-slug [{params :query-params}] + (let [slug (get params "slug")] + {:status 200 + :body (snippets.use-cases.view/view-snippet-by-slug slug)})) + (defn wrap [handler id] (fn [request] (update (handler request) :wrap (fnil conj '()) id))) @@ -64,6 +79,8 @@ [wrap :api]]} ["/ping" {:get handle-ping}] ["/tags" {:get handle-view-tags}] + ["/tag" {:get handle-view-snippets-by-tag}] + ["/snippet-by-slug" {:get handle-view-snippet-by-slug}] ["/snippets" {:get handle-view-snippets}] ["/snippet" {:post handle-create-snippet :get handle-view-snippet diff --git a/src/snippets/infra/db.clj b/src/snippets/infra/db.clj index 537ce1b..8346ef5 100644 --- a/src/snippets/infra/db.clj +++ b/src/snippets/infra/db.clj @@ -24,6 +24,9 @@ (defn get-snippet-by-id [snippet-id] (first (xt/q client ['#(from :snippets [{:xt/id %} slug title tags {:xt/id id} markdown pub-date]) snippet-id]))) +(defn get-snippet-by-slug [slug] + (first (xt/q client ['#(from :snippets [{:xt/id id} {:slug %} slug title tags markdown pub-date]) slug]))) + (defn put-snippet [id snippet] (t/log! {:level :info, :data {:snippet snippet :id id}} "Saving new snippet to db") (xt/execute-tx client [[:put-docs :snippets (merge snippet {:xt/id id})]])) @@ -43,3 +46,8 @@ (defn list-tags [] ;; (xt/q client '(-> (from :snippets [{:xt/id id} tags]) (unnest {:tag tags}) (without :tags) (aggregate tag {:ids (array-agg id)})))) (xt/q client '(-> (from :snippets [{:xt/id id} tags]) (unnest {:tag tags}) (without :tags) (aggregate tag {:count (count id)})))) + +(defn get-snippets-by-tag [tag] + (map #(dissoc % :tag) + (xt/q client (eval (read-string + (format "(quote (-> (from :snippets [title slug tags pub-date]) (unnest {:tag tags}) (without :tags) (where (= tag \"%s\"))))" tag)))))) diff --git a/src/snippets/use_cases/view.clj b/src/snippets/use_cases/view.clj index 98b66a6..4edbacd 100644 --- a/src/snippets/use_cases/view.clj +++ b/src/snippets/use_cases/view.clj @@ -13,3 +13,11 @@ (defn view-tags [] (t/log! {:level :info} "Viewing tags") (db/list-tags)) + +(defn view-snippets-by-tag [tag] + (t/log! {:level :info :data {:tag tag}} "Viewing snippet by tag") + (db/get-snippets-by-tag tag)) + +(defn view-snippet-by-slug [slug] + (t/log! {:level :info :data {:slug slug}} "Viewing snippet by slug") + (db/get-snippet-by-slug slug)) From f3e03e43c74d0c02335a655f0ac61dead53c872e Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Mon, 16 Jun 2025 19:10:37 +0200 Subject: [PATCH 07/10] update readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 093d70c..7d3eb31 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Code Snippets -App to store my code snippets. +Backend application to store my code snippets and make them available via REST API. + +[CLI CMS Companion Project](https://git.sr.ht/~travisshears/code-snippets-cli-cms) + +This project is written in [Clojure](https://clojure.org/) and data is stored in [XTDB](https://xtdb.dev/). ## Links From 81f047ef0c80de81c0570f3282eb922ce68067a0 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Tue, 12 Aug 2025 14:25:59 +0200 Subject: [PATCH 08/10] clean up deps file --- deps.edn | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deps.edn b/deps.edn index 6b12105..fbc464d 100644 --- a/deps.edn +++ b/deps.edn @@ -1,7 +1,7 @@ {:paths ["src"] - :deps {ring/ring-core {:mvn/version "1.13.0"} + :deps {;; api + ring/ring-core {:mvn/version "1.13.0"} ring/ring-jetty-adapter {:mvn/version "1.13.0"} - ;; logging, required by jetty: org.slf4j/slf4j-simple {:mvn/version "2.0.16"} ;; db From ca5ad88fd23935b684ab6a3860f1f8b86bad3445 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Sun, 17 Aug 2025 12:22:13 +0200 Subject: [PATCH 09/10] update docker image tag --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d3eb31..c582409 100644 --- a/README.md +++ b/README.md @@ -26,5 +26,6 @@ $ clojure -M -m snippets.infra.api At this point I'm using a private AWS ECR repository for the project and deploying it in my homelab. ```shell -$ docker buildx build --platform linux/amd64,linux/arm64 -t 853019563312.dkr.ecr.eu-central-1.amazonaws.com/code-snippets:latest --push . +$ export AWS_PROFILE=personal +$ docker buildx build --platform linux/amd64,linux/arm64 -t 853019563312.dkr.ecr.eu-central-1.amazonaws.com/snippets-homelabstack:latest --push . ``` From 56d45ad311562c6474cedc6850674b907d329e27 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Sat, 1 Nov 2025 17:07:20 +0100 Subject: [PATCH 10/10] add build script --- build.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100755 build.sh diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..71da131 --- /dev/null +++ b/build.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +export AWS_PROFILE=personal +export AWS_REGION=eu-central-1 + +REPO_NAME="snippets-homelabstack" + +if ! aws ecr describe-repositories --repository-names "$REPO_NAME" >/dev/null 2>&1; then + aws ecr create-repository --repository-name "$REPO_NAME" +fi + +docker buildx build --platform linux/amd64,linux/arm64 -t "853019563312.dkr.ecr.eu-central-1.amazonaws.com/${REPO_NAME}:latest" --push . + +echo "Docker image built and pushed to AWS ECR"