Initial linear code.

This commit is contained in:
Oliver 2026-09-29 11:23:20 +01:00
parent a36202b167
commit 83edc12709
5 changed files with 233 additions and 0 deletions

View File

@ -0,0 +1,4 @@
{:paths ["src" "resources"]
:deps {}
:aliases {:test {:extra-paths ["test"]
:extra-deps {}}}}

View File

@ -0,0 +1,59 @@
(ns org.aplex.rest-linear.core
(:require [org.aplex.helpers.interface :as h]))
(def default-config {:api-host "https://api.linear.app"})
(def api-config (atom default-config))
(defn init-config [cfg]
(reset! api-config (merge default-config cfg)))
(defn- api-key []
(or (-> @api-config :api-key)
(throw (ex-info "Linear api_key is not configured; call init-config with :api-key"
{:component "org.aplex.rest-linear"}))))
(defn- request [query variables]
(let [envelope (cond-> {:query query}
variables (assoc :variables variables))]
{:method :post
:base-url (-> @api-config :api-host)
:path "/graphql"
:path-params []
:query-params []
:headers [{:name "Authorization" :value (api-key)}]
:auth nil
:body (when envelope (h/encode-json envelope))}))
(defn fetch-teams []
(request "query{ teams{ nodes{ id name key } } }" nil))
(defn fetch-issue [issue-id]
(request "query($id: String!){ issue(id: $id){ id identifier title description state{ name } } }"
{:id issue-id}))
(defn fetch-my-issues []
(request "query{ viewer{ assignedIssues{ nodes{ id identifier title state{ name } } } } }" nil))
(defn fetch-team-issues [team-id]
(request "query($id: String!){ team(id: $id){ id name key issues{ nodes{ id identifier title state{ name } } } } }"
{:id team-id}))
(defn fetch-active-cycle-issues [team-id]
(request "query($id: String!){ team(id: $id){ activeCycle{ id number issues{ nodes{ id identifier title state{ name } assignee{ name } } } } } }"
{:id team-id}))
(defn create-issue
([title team-id]
(create-issue title team-id nil))
([title team-id description]
(request "mutation($input: IssueCreateInput!){ issueCreate(input: $input){ success issue{ id identifier } } }"
(cond-> {:input {:title title :teamId team-id}}
description (assoc-in [:input :description] description)))))
(defn update-issue [issue-id updates]
(request "mutation($id: String!, $input: IssueUpdateInput!){ issueUpdate(id: $id, input: $input){ success issue{ id identifier } } }"
{:id issue-id :input updates}))
(defn fetch-issues [filter]
(request "query($filter: IssueFilter){ issues(filter: $filter){ nodes{ id identifier title state{ name } assignee{ name email } } } }"
{:filter filter}))

View File

@ -0,0 +1,33 @@
(ns org.aplex.rest-linear.interface
(:require
[org.aplex.rest-linear.core :as c]))
(defn init-config [cfg]
(c/init-config cfg))
(defn fetch-teams []
(c/fetch-teams))
(defn fetch-issue [issue-id]
(c/fetch-issue issue-id))
(defn fetch-my-issues []
(c/fetch-my-issues))
(defn fetch-team-issues [team-id]
(c/fetch-team-issues team-id))
(defn fetch-active-cycle-issues [team-id]
(c/fetch-active-cycle-issues team-id))
(defn create-issue
([title team-id]
(c/create-issue title team-id))
([title team-id description]
(c/create-issue title team-id description)))
(defn update-issue [issue-id updates]
(c/update-issue issue-id updates))
(defn fetch-issues [filter]
(c/fetch-issues filter))

View File

@ -0,0 +1,137 @@
(ns org.aplex.rest-linear.interface-test
(:require [clojure.test :as test :refer :all]
[org.aplex.rest-linear.interface :as rest-linear]))
(def abstract-keys
#{:method :base-url :path :path-params :query-params :headers :auth :body})
(defn reset-config [cfg]
(rest-linear/init-config cfg))
(defn test-config []
{:api-key "lin_api_test"})
(defn api-key-header []
[{:name "Authorization" :value "lin_api_test"}])
(deftest api-host-defaults-to-linear
(reset-config (test-config))
(is (= "https://api.linear.app"
(:base-url (rest-linear/fetch-teams)))))
(deftest api-host-is-overridable
(reset-config (merge (test-config) {:api-host "https://linear.example.com"}))
(is (= "https://linear.example.com"
(:base-url (rest-linear/fetch-teams)))))
(deftest api-key-is-mandatory
(reset-config {:api-key nil})
(is (thrown? Exception (rest-linear/fetch-teams))))
(deftest fetch-teams-builds-abstract-request
(reset-config (test-config))
(is (= {:method :post
:base-url "https://api.linear.app"
:path "/graphql"
:path-params []
:query-params []
:headers (api-key-header)
:auth nil
:body "{\"query\":\"query{ teams{ nodes{ id name key } } }\"}"}
(rest-linear/fetch-teams))))
(deftest fetch-issue-builds-abstract-request
(reset-config (test-config))
(is (= {:method :post
:base-url "https://api.linear.app"
:path "/graphql"
:path-params []
:query-params []
:headers (api-key-header)
:auth nil
:body "{\"query\":\"query($id: String!){ issue(id: $id){ id identifier title description state{ name } } }\",\"variables\":{\"id\":\"ABC-123\"}}"}
(rest-linear/fetch-issue "ABC-123"))))
(deftest fetch-my-issues-builds-abstract-request
(reset-config (test-config))
(is (= {:method :post
:base-url "https://api.linear.app"
:path "/graphql"
:path-params []
:query-params []
:headers (api-key-header)
:auth nil
:body "{\"query\":\"query{ viewer{ assignedIssues{ nodes{ id identifier title state{ name } } } } }\"}"}
(rest-linear/fetch-my-issues))))
(deftest fetch-team-issues-builds-abstract-request
(reset-config (test-config))
(is (= {:method :post
:base-url "https://api.linear.app"
:path "/graphql"
:path-params []
:query-params []
:headers (api-key-header)
:auth nil
:body "{\"query\":\"query($id: String!){ team(id: $id){ id name key issues{ nodes{ id identifier title state{ name } } } } }\",\"variables\":{\"id\":\"team-1\"}}"}
(rest-linear/fetch-team-issues "team-1"))))
(deftest create-issue-builds-abstract-request
(reset-config (test-config))
(is (= {:method :post
:base-url "https://api.linear.app"
:path "/graphql"
:path-params []
:query-params []
:headers (api-key-header)
:auth nil
:body "{\"query\":\"mutation($input: IssueCreateInput!){ issueCreate(input: $input){ success issue{ id identifier } } }\",\"variables\":{\"input\":{\"title\":\"My title\",\"teamId\":\"team-1\"}}}"}
(rest-linear/create-issue "My title" "team-1"))))
(deftest create-issue-includes-description-when-given
(reset-config (test-config))
(is (= {:method :post
:base-url "https://api.linear.app"
:path "/graphql"
:path-params []
:query-params []
:headers (api-key-header)
:auth nil
:body "{\"query\":\"mutation($input: IssueCreateInput!){ issueCreate(input: $input){ success issue{ id identifier } } }\",\"variables\":{\"input\":{\"title\":\"My title\",\"teamId\":\"team-1\",\"description\":\"A description\"}}}"}
(rest-linear/create-issue "My title" "team-1" "A description"))))
(deftest update-issue-builds-abstract-request
(reset-config (test-config))
(is (= {:method :post
:base-url "https://api.linear.app"
:path "/graphql"
:path-params []
:query-params []
:headers (api-key-header)
:auth nil
:body "{\"query\":\"mutation($id: String!, $input: IssueUpdateInput!){ issueUpdate(id: $id, input: $input){ success issue{ id identifier } } }\",\"variables\":{\"id\":\"issue-1\",\"input\":{\"title\":\"New title\"}}}"}
(rest-linear/update-issue "issue-1" {:title "New title"}))))
(deftest fetch-active-cycle-issues-builds-abstract-request
(reset-config (test-config))
(is (= {:method :post
:base-url "https://api.linear.app"
:path "/graphql"
:path-params []
:query-params []
:headers (api-key-header)
:auth nil
:body "{\"query\":\"query($id: String!){ team(id: $id){ activeCycle{ id number issues{ nodes{ id identifier title state{ name } assignee{ name } } } } } }\",\"variables\":{\"id\":\"team-1\"}}"}
(rest-linear/fetch-active-cycle-issues "team-1"))))
(deftest requests-are-transport-agnostic
(reset-config (test-config))
(doseq [request [(rest-linear/fetch-teams)
(rest-linear/fetch-issue "ABC-123")
(rest-linear/fetch-my-issues)
(rest-linear/fetch-team-issues "team-1")
(rest-linear/fetch-active-cycle-issues "team-1")
(rest-linear/create-issue "My title" "team-1")
(rest-linear/update-issue "issue-1" {:title "New title"})]]
(is (= abstract-keys (set (keys request)))
(str "request carries only abstract fields: " request))))