Merge remote-tracking branch 'origin/master' into feat/cliable

This commit is contained in:
rcmerci
2026-04-28 22:59:59 +08:00
77 changed files with 4549 additions and 1133 deletions

View File

@@ -534,6 +534,16 @@
[value]
(contains? #{"true" "1"} value))
(defn- sqlite-too-big-error?
[error]
(let [message (-> (or (ex-message error)
(some-> error .-message)
(str error))
string/lower-case)]
(or (string/includes? message "sqlite_toobig")
(string/includes? message "string or blob too big")
(string/includes? message "statement too long"))))
(defn- handle-sync-snapshot-upload
[^js self request url]
(let [graph-id (graph-id-from-request request)
@@ -556,27 +566,27 @@
(if (and (= encoding snapshot-content-encoding)
(not (exists? js/DecompressionStream)))
(http/error-response "gzip not supported" 500)
(p/let [_ (ensure-schema! self)
_ (when reset?
(storage/set-meta! (.-sql self) snapshot-uploading-meta-key true))
_ (when reset?
(<set-graph-ready-for-use! self graph-id false))
stream (maybe-decompress-stream stream encoding)
count (import-snapshot-stream! self stream reset?)
_ (when finished?
(storage/set-meta! (.-sql self) snapshot-uploading-meta-key false))
_ (when finished?
(when (seq checksum-param)
(storage/set-checksum! (.-sql self) checksum-param)))
_ (when finished?
(<set-graph-ready-for-use! self graph-id true))
_ (when finished?
;; Snapshot replacement resets tx history (`t` may drop to 0).
;; Broadcast current `t` so connected clients can recover.
(ws/broadcast! self nil {:type "changed"
:t (t-now self)}))]
(http/json-response :sync/snapshot-upload {:ok true
:count count})))))))
(p/catch
(p/let [_ (ensure-schema! self)
_ (when reset?
(storage/set-meta! (.-sql self) snapshot-uploading-meta-key true))
_ (when reset?
(<set-graph-ready-for-use! self graph-id false))
stream (maybe-decompress-stream stream encoding)
count (import-snapshot-stream! self stream reset?)
_ (when finished?
(storage/set-meta! (.-sql self) snapshot-uploading-meta-key false))
_ (when finished?
(when (seq checksum-param)
(storage/set-checksum! (.-sql self) checksum-param)))
_ (when finished?
(<set-graph-ready-for-use! self graph-id true))]
(http/json-response :sync/snapshot-upload {:ok true
:count count}))
(fn [error]
(if (sqlite-too-big-error? error)
(http/error-response "snapshot row too large" 413)
(throw error)))))))))
(defn handle [{:keys [^js self request url route]}]
(case (:handler route)

View File

@@ -661,11 +661,10 @@
(is false (str error))
(done)))))))
(deftest finished-snapshot-upload-broadcasts-changed-test
(deftest snapshot-upload-returns-413-when-sqlite-row-is-too-large-test
(async done
(let [sql (test-sql/make-sql)
conn (d/create-conn db-schema/schema)
changed-messages (atom [])
self #js {:sql sql
:conn conn
:schema-ready true
@@ -674,19 +673,17 @@
#js {:method "POST"
:body (js/Uint8Array. 0)})]
(-> (p/with-redefs [sync-handler/import-snapshot-stream! (fn [_self _stream _reset?]
(p/resolved 0))
(p/rejected (js/Error. "string or blob too big: SQLITE_TOOBIG")))
sync-handler/<set-graph-ready-for-use! (fn [_self _graph-id _graph-ready-for-use?]
(p/resolved true))
ws/broadcast! (fn [_self _sender payload]
(swap! changed-messages conj payload))]
(p/resolved true))]
(p/let [resp (sync-handler/handle {:self self
:request request
:url (js/URL. (.-url request))
:route {:handler :sync/snapshot-upload}})]
(is (= 200 (.-status resp)))
(is (= [{:type "changed"
:t (storage/get-t sql)}]
@changed-messages))))
:route {:handler :sync/snapshot-upload}})
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)]
(is (= 413 (.-status resp)))
(is (= {:error "snapshot row too large"} body))))
(p/then (fn []
(done)))
(p/catch (fn [error]

View File

@@ -2,7 +2,6 @@
:api-namespaces [logseq.outliner.datascript-report
logseq.outliner.pipeline
logseq.outliner.cli
logseq.outliner.batch-tx
logseq.outliner.core
logseq.outliner.db-pipeline
logseq.outliner.property

View File

@@ -1,4 +1,4 @@
{:paths ["src" "../../resources"]
{:paths ["src" "test" "../../resources"]
:deps
{org.clojure/clojure {:mvn/version "1.12.4"}
rum/rum {:git/url "https://github.com/logseq/rum" ;; fork

View File

@@ -7,11 +7,13 @@
"dev": "cd ./worker && pnpm exec wrangler dev",
"watch": "clojure -M:cljs watch publish-worker",
"release": "clojure -M:cljs release publish-worker",
"test": "clojure -M:cljs compile publish-test && node worker/dist/worker-test.js",
"clean": "rm -rf ./worker/dist/",
"bump-publish-version": "node ./scripts/bump-publish-version.js",
"deploy": "pnpm bump-publish-version && pnpm clean && pnpm release && cd ./worker && pnpm exec wrangler deploy --env prod"
},
"dependencies": {
"mldoc": "^1.5.9",
"shadow-cljs": "^3.4.4"
}
}

412
deps/publish/pnpm-lock.yaml generated vendored
View File

@@ -8,12 +8,23 @@ importers:
.:
dependencies:
mldoc:
specifier: ^1.5.9
version: 1.5.9
shadow-cljs:
specifier: ^3.4.4
version: 3.4.4
packages:
ansi-regex@2.1.1:
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
engines: {node: '>=0.10.0'}
ansi-regex@3.0.1:
resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==}
engines: {node: '>=4'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -23,21 +34,168 @@ packages:
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
cliui@4.1.0:
resolution: {integrity: sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==}
code-point-at@1.1.0:
resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==}
engines: {node: '>=0.10.0'}
cross-spawn@6.0.6:
resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==}
engines: {node: '>=4.8'}
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
execa@1.0.0:
resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==}
engines: {node: '>=6'}
find-up@3.0.0:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
get-caller-file@1.0.3:
resolution: {integrity: sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==}
get-stream@4.1.0:
resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==}
engines: {node: '>=6'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
invert-kv@2.0.0:
resolution: {integrity: sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==}
engines: {node: '>=4'}
is-fullwidth-code-point@1.0.0:
resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==}
engines: {node: '>=0.10.0'}
is-fullwidth-code-point@2.0.0:
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
engines: {node: '>=4'}
is-stream@1.1.0:
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
engines: {node: '>=0.10.0'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isexe@3.1.1:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
lcid@2.0.0:
resolution: {integrity: sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==}
engines: {node: '>=6'}
locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
map-age-cleaner@0.1.3:
resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==}
engines: {node: '>=6'}
mem@4.3.0:
resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==}
engines: {node: '>=6'}
mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
mldoc@1.5.9:
resolution: {integrity: sha512-87FQ7hseS87tsk+VdpIigpu8LH+GwmbbFgpxgFwvnbH5oOjmIrc47laH4Dyggzqiy8/vMjDHkl7vsId0eXhCDQ==}
hasBin: true
nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
npm-run-path@2.0.2:
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
engines: {node: '>=4'}
number-is-nan@1.0.1:
resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==}
engines: {node: '>=0.10.0'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
os-locale@3.1.0:
resolution: {integrity: sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==}
engines: {node: '>=6'}
p-defer@1.0.0:
resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==}
engines: {node: '>=4'}
p-finally@1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
p-is-promise@2.1.0:
resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==}
engines: {node: '>=6'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-locate@3.0.0:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
path-key@2.0.1:
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
engines: {node: '>=4'}
process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
readline-sync@1.4.10:
resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==}
engines: {node: '>= 0.8.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@1.0.1:
resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==}
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
shadow-cljs-jar@1.3.4:
resolution: {integrity: sha512-cZB2pzVXBnhpJ6PQdsjO+j/MksR28mv4QD/hP/2y1fsIa9Z9RutYgh3N34FZ8Ktl4puAXaIGlct+gMCJ5BmwmA==}
@@ -46,6 +204,17 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
shebang-command@1.2.0:
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'}
shebang-regex@1.0.0:
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
engines: {node: '>=0.10.0'}
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
@@ -53,11 +222,45 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
string-width@1.0.2:
resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==}
engines: {node: '>=0.10.0'}
string-width@2.1.1:
resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==}
engines: {node: '>=4'}
strip-ansi@3.0.1:
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
engines: {node: '>=0.10.0'}
strip-ansi@4.0.0:
resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==}
engines: {node: '>=4'}
strip-eof@1.0.0:
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
engines: {node: '>=0.10.0'}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
which@5.0.0:
resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==}
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
wrap-ansi@2.1.0:
resolution: {integrity: sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==}
engines: {node: '>=0.10.0'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
@@ -70,8 +273,21 @@ packages:
utf-8-validate:
optional: true
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yargs-parser@11.1.1:
resolution: {integrity: sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==}
yargs@12.0.5:
resolution: {integrity: sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==}
snapshots:
ansi-regex@2.1.1: {}
ansi-regex@3.0.1: {}
base64-js@1.5.1: {}
buffer-from@1.1.2: {}
@@ -81,14 +297,146 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
camelcase@5.3.1: {}
cliui@4.1.0:
dependencies:
string-width: 2.1.1
strip-ansi: 4.0.0
wrap-ansi: 2.1.0
code-point-at@1.1.0: {}
cross-spawn@6.0.6:
dependencies:
nice-try: 1.0.5
path-key: 2.0.1
semver: 5.7.2
shebang-command: 1.2.0
which: 1.3.1
decamelize@1.2.0: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
execa@1.0.0:
dependencies:
cross-spawn: 6.0.6
get-stream: 4.1.0
is-stream: 1.1.0
npm-run-path: 2.0.2
p-finally: 1.0.0
signal-exit: 3.0.7
strip-eof: 1.0.0
find-up@3.0.0:
dependencies:
locate-path: 3.0.0
get-caller-file@1.0.3: {}
get-stream@4.1.0:
dependencies:
pump: 3.0.4
ieee754@1.2.1: {}
invert-kv@2.0.0: {}
is-fullwidth-code-point@1.0.0:
dependencies:
number-is-nan: 1.0.1
is-fullwidth-code-point@2.0.0: {}
is-stream@1.1.0: {}
isexe@2.0.0: {}
isexe@3.1.1: {}
lcid@2.0.0:
dependencies:
invert-kv: 2.0.0
locate-path@3.0.0:
dependencies:
p-locate: 3.0.0
path-exists: 3.0.0
map-age-cleaner@0.1.3:
dependencies:
p-defer: 1.0.0
mem@4.3.0:
dependencies:
map-age-cleaner: 0.1.3
mimic-fn: 2.1.0
p-is-promise: 2.1.0
mimic-fn@2.1.0: {}
mldoc@1.5.9:
dependencies:
yargs: 12.0.5
nice-try@1.0.5: {}
npm-run-path@2.0.2:
dependencies:
path-key: 2.0.1
number-is-nan@1.0.1: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
os-locale@3.1.0:
dependencies:
execa: 1.0.0
lcid: 2.0.0
mem: 4.3.0
p-defer@1.0.0: {}
p-finally@1.0.0: {}
p-is-promise@2.1.0: {}
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-locate@3.0.0:
dependencies:
p-limit: 2.3.0
p-try@2.2.0: {}
path-exists@3.0.0: {}
path-key@2.0.1: {}
process@0.11.10: {}
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
readline-sync@1.4.10: {}
require-directory@2.1.1: {}
require-main-filename@1.0.1: {}
semver@5.7.2: {}
set-blocking@2.0.0: {}
shadow-cljs-jar@1.3.4: {}
shadow-cljs@3.4.4:
@@ -104,6 +452,14 @@ snapshots:
- bufferutil
- utf-8-validate
shebang-command@1.2.0:
dependencies:
shebang-regex: 1.0.0
shebang-regex@1.0.0: {}
signal-exit@3.0.7: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
@@ -111,8 +467,64 @@ snapshots:
source-map@0.6.1: {}
string-width@1.0.2:
dependencies:
code-point-at: 1.1.0
is-fullwidth-code-point: 1.0.0
strip-ansi: 3.0.1
string-width@2.1.1:
dependencies:
is-fullwidth-code-point: 2.0.0
strip-ansi: 4.0.0
strip-ansi@3.0.1:
dependencies:
ansi-regex: 2.1.1
strip-ansi@4.0.0:
dependencies:
ansi-regex: 3.0.1
strip-eof@1.0.0: {}
which-module@2.0.1: {}
which@1.3.1:
dependencies:
isexe: 2.0.0
which@5.0.0:
dependencies:
isexe: 3.1.1
wrap-ansi@2.1.0:
dependencies:
string-width: 1.0.2
strip-ansi: 3.0.1
wrappy@1.0.2: {}
ws@8.18.3: {}
y18n@4.0.3: {}
yargs-parser@11.1.1:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@12.0.5:
dependencies:
cliui: 4.1.0
decamelize: 1.2.0
find-up: 3.0.0
get-caller-file: 1.0.3
os-locale: 3.1.0
require-directory: 2.1.1
require-main-filename: 1.0.1
set-blocking: 2.0.0
string-width: 2.1.1
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 11.1.1

View File

@@ -9,4 +9,9 @@
PublishMetaDO logseq.publish.worker/PublishMetaDO}}}
:js-options {:js-provider :import}
:closure-defines {shadow.cljs.devtools.client.env/enabled false}
:devtools {:enabled false}}}}
:devtools {:enabled false}}
:publish-test {:target :node-test
:output-to "worker/dist/worker-test.js"
:devtools {:enabled false}
:compiler-options {:static-fns false}
:main logseq.publish.test-runner/main}}}

View File

@@ -281,10 +281,11 @@
"page_tags.source_block_content, page_tags.source_block_format, page_tags.updated_at, "
"pages.short_id "
"FROM page_tags "
"LEFT JOIN pages "
"INNER JOIN pages "
"ON pages.graph_uuid = page_tags.graph_uuid "
"AND pages.page_uuid = page_tags.source_page_uuid "
"WHERE page_tags.tag_title = ? "
"AND pages.password_hash IS NULL "
"ORDER BY page_tags.updated_at DESC;")
tag-name))
page-rows (publish-common/get-sql-rows
@@ -293,10 +294,11 @@
"pages.short_id, "
"MAX(page_tags.updated_at) AS updated_at "
"FROM page_tags "
"LEFT JOIN pages "
"INNER JOIN pages "
"ON pages.graph_uuid = page_tags.graph_uuid "
"AND pages.page_uuid = page_tags.source_page_uuid "
"WHERE page_tags.tag_title = ? "
"AND pages.password_hash IS NULL "
"GROUP BY page_tags.graph_uuid, page_tags.source_page_uuid, page_tags.source_page_title, pages.short_id "
"ORDER BY updated_at DESC;")
tag-name))]
@@ -316,11 +318,12 @@
"pages.short_id, "
"MAX(page_refs.updated_at) AS updated_at "
"FROM page_refs "
"LEFT JOIN pages "
"INNER JOIN pages "
"ON pages.graph_uuid = page_refs.graph_uuid "
"AND pages.page_uuid = page_refs.source_page_uuid "
"WHERE (lower(page_refs.target_page_title) = lower(?)) "
"OR (page_refs.target_page_name = lower(?)) "
"WHERE ((lower(page_refs.target_page_title) = lower(?)) "
"OR (page_refs.target_page_name = lower(?))) "
"AND pages.password_hash IS NULL "
"GROUP BY page_refs.graph_uuid, page_refs.source_page_uuid, page_refs.source_page_title, pages.short_id "
"ORDER BY updated_at DESC;")
ref-name
@@ -345,7 +348,10 @@
rows (publish-common/get-sql-rows
(publish-common/sql-exec sql
(str "SELECT page_uuid, page_title, short_id, graph_uuid, updated_at, owner_username "
"FROM pages WHERE owner_username = ? ORDER BY updated_at DESC;")
"FROM pages "
"WHERE owner_username = ? "
"AND password_hash IS NULL "
"ORDER BY updated_at DESC;")
username))]
(publish-common/json-response {:user {:username username}
:pages (map (fn [row]
@@ -381,11 +387,16 @@
(= (nth parts 4 nil) "tagged_nodes")
(let [rows (publish-common/get-sql-rows
(publish-common/sql-exec sql
(str "SELECT graph_uuid, tag_page_uuid, tag_title, source_page_uuid, "
"source_page_title, source_block_uuid, source_block_content, "
"source_block_format, updated_at "
"FROM page_tags WHERE graph_uuid = ? AND tag_page_uuid = ? "
"ORDER BY updated_at DESC;")
(str "SELECT page_tags.graph_uuid, page_tags.tag_page_uuid, page_tags.tag_title, page_tags.source_page_uuid, "
"page_tags.source_page_title, page_tags.source_block_uuid, page_tags.source_block_content, "
"page_tags.source_block_format, page_tags.updated_at "
"FROM page_tags "
"INNER JOIN pages "
"ON pages.graph_uuid = page_tags.graph_uuid "
"AND pages.page_uuid = page_tags.source_page_uuid "
"WHERE page_tags.graph_uuid = ? AND page_tags.tag_page_uuid = ? "
"AND pages.password_hash IS NULL "
"ORDER BY page_tags.updated_at DESC;")
graph-uuid
page-uuid))]
(publish-common/json-response {:tagged_nodes (map (fn [row]

View File

@@ -72,6 +72,10 @@ body {
letter-spacing: 0.01em;
}
.inline-flex {
display: inline-flex;
}
.publish-home {
display: flex;
align-items: center;
@@ -474,6 +478,7 @@ a:hover {
display: flex;
gap: 4px;
align-items: flex-start;
min-height: 26px;
}
.positioned-properties {
@@ -486,7 +491,10 @@ a:hover {
.positioned-properties.block-left, .positioned-properties.block-right {
display: flex;
align-items: center;
margin-top: 2px;
}
.block-content > .positioned-properties.block-left {
align-self: center;
}
.positioned-properties.block-right {
@@ -520,16 +528,40 @@ a:hover {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
width: 18px;
height: 18px;
line-height: 1;
font-size: 1rem;
font-size: 18px;
color: currentColor;
}
.property-icon svg {
width: 1rem;
height: 1rem;
width: 18px;
height: 18px;
}
.positioned-properties .ls-icon-Backlog {
color: var(--rx-gray-05, #a8a29e);
}
.positioned-properties .ls-icon-Todo {
color: var(--rx-gray-10, #78716c);
}
.positioned-properties .ls-icon-InProgress50 {
color: var(--rx-yellow-08, #ca8a04);
}
.positioned-properties .ls-icon-InReview {
color: var(--rx-blue-09, #1d4ed8);
}
.positioned-properties .ls-icon-Done {
color: var(--rx-green-08, #16a34a);
}
.positioned-properties .ls-icon-Cancelled {
color: var(--rx-red-08, #dc2626);
}
.property-value-with-icon {

View File

@@ -151,7 +151,7 @@ const getTablerExtIcon = (id) => {
const renderTablerExtIcon = (el, id) => {
const iconFn = getTablerExtIcon(id);
if (!iconFn) return false;
const node = iconFn({ size: 14, stroke: 2 });
const node = iconFn({ size: 18 });
if (!node) return false;
el.textContent = "";
const domNode = reactNodeToDom(node);

View File

@@ -11,7 +11,7 @@
[logseq.publish.model :as publish-model]))
;; Timestamp in milliseconds used for cache busting static assets.
(defonce version 1767194868810)
(defonce version 1777365821532)
(def ref-regex
(js/RegExp. "\\[\\[([0-9a-fA-F-]{36})\\]\\]|\\(\\(([0-9a-fA-F-]{36})\\)\\)" "g"))
@@ -100,13 +100,19 @@
(defn- icon-span
[icon]
(when (and (map? icon) (string? (:id icon)) (not (string/blank? (:id icon))))
(let [icon-type (:type icon)
icon-id (:id icon)
class-name (str "property-icon"
(when (and (= :tabler-icon icon-type)
(not (string/blank? icon-id)))
(str " ls-icon-" icon-id)))]
[:span
(cond->
{:class "property-icon"
{:class class-name
:data-icon-id (:id icon)
:data-icon-type (name (:type icon))}
(:color icon)
(assoc :style (str "color: " (:color icon) ";")))]))
(assoc :style (str "color: " (:color icon) ";")))])))
(defn- with-icon
[icon nodes]
@@ -255,6 +261,11 @@
(or (get property-type-by-ident prop-key)
(get-in db-property/built-in-properties [prop-key :schema :type])))
(defn property-hidden?
[prop-key property-hidden-by-ident]
(or (true? (get property-hidden-by-ident prop-key))
(true? (get-in db-property/built-in-properties [prop-key :schema :hide?]))))
(defn page-ref->uuid [name name->uuid]
(or (get name->uuid name)
(get name->uuid (common-util/page-name-sanity-lc name))))
@@ -351,7 +362,7 @@
props)
props (->> props
(remove (fn [[k _]]
(true? (get (:property-hidden-by-ident ctx) k))))
(property-hidden? k (:property-hidden-by-ident ctx))))
(map (fn [[k v]]
(if (= k :block/tags)
[k (filter-tags v entities)]
@@ -445,12 +456,12 @@
[:div.positioned-property
[:span.property-name (property-title k (:property-title-by-ident ctx))]
[:span.property-value
(into [:span] (positioned-value-nodes v k ctx entities))]])]
(into [:span.inline-flex] (positioned-value-nodes v k ctx entities))]])]
[:div {:class (str "positioned-properties " (name position))}
(for [[k v] (sorted-properties props ctx)]
[:span.positioned-property
(into [:span] (positioned-value-nodes v k ctx entities))])])))
(into [:span.inline-flex] (positioned-value-nodes v k ctx entities))])])))
(def ^:private youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$")
(def ^:private vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$")

View File

@@ -312,20 +312,6 @@
(p/let [tags (.json tags-resp)]
(publish-common/json-response (js->clj tags :keywordize-keys true) 200))))))))))
(defn handle-list-pages [env]
(let [^js do-ns (aget env "PUBLISH_META_DO")
do-id (.idFromName do-ns "index")
do-stub (.get do-ns do-id)]
(p/let [meta-resp (.fetch do-stub "https://publish/pages" #js {:method "GET"})]
(if-not (.-ok meta-resp)
(js/Response.
(publish-render/render-404-html)
#js {:headers (publish-common/merge-headers
#js {"content-type" "text/html; charset=utf-8"}
(publish-common/cors-headers))})
(p/let [meta (.json meta-resp)]
(publish-common/json-response (js->clj meta :keywordize-keys true) 200))))))
(defn handle-list-graph-pages-by-uuid [graph-uuid env]
(if-not graph-uuid
(publish-common/bad-request "missing graph uuid")
@@ -620,6 +606,13 @@
(publish-render/render-page-html transit page-uuid refs-json tagged-nodes)
#js {:headers headers}))))))))))))))
(defn- rewrite-request-path
[request new-path]
(let [request-url (js/URL. (.-url request))
new-url (js/URL. (str (.-origin request-url) new-path))]
(set! (.-search new-url) (.-search request-url))
(js/Request. (str new-url) request)))
(defn ^:large-vars/cleanup-todo handle-fetch [request env]
(let [url (js/URL. (.-url request))
path (.-pathname url)
@@ -669,9 +662,6 @@
(and (= path "/pages") (= method "POST"))
(handle-post-pages request env)
(and (= path "/pages") (= method "GET"))
(handle-list-pages env)
(and (string/starts-with? path "/search/") (= method "GET"))
(handle-graph-search request env)
@@ -726,7 +716,9 @@
(js/Response. (.-body object)
#js {:headers headers}))))))))
(and (string/starts-with? path "/p/") (= method "GET"))
(and (or (string/starts-with? path "/p/")
(string/starts-with? path "/s/"))
(= method "GET"))
(let [parts (string/split path #"/")
short-id (nth parts 2 nil)]
(if (string/blank? short-id)
@@ -744,11 +736,9 @@
(publish-common/not-found)
(let [graph-uuid (aget row "graph_uuid")
page-uuid (aget row "page_uuid")
location (str "/page/" graph-uuid "/" page-uuid)]
(js/Response. nil #js {:status 302
:headers (publish-common/merge-headers
#js {"location" location}
(publish-common/cors-headers))})))))))))
page-path (str "/page/" graph-uuid "/" page-uuid)
page-request (rewrite-request-path request page-path)]
(handle-page-html page-request env)))))))))
(and (string/starts-with? path "/u/") (= method "GET"))
(let [parts (string/split path #"/")

View File

@@ -0,0 +1,100 @@
(ns logseq.publish.common-test
(:require [cljs.test :refer [async deftest is testing]]
[logseq.publish.common :as publish-common]
[promesa.core :as p]))
(deftest merge-headers-overrides-and-preserves-values
(let [headers (publish-common/merge-headers #js {"a" "1" "keep" "ok"}
#js {"a" "2" "b" "3"})]
(is (= "2" (.get headers "a")))
(is (= "3" (.get headers "b")))
(is (= "ok" (.get headers "keep")))))
(deftest parse-meta-header-valid-and-invalid-json
(testing "valid json meta header is parsed into keywordized map"
(let [request (js/Request. "https://publish.example/pages"
#js {:headers #js {"x-publish-meta" "{\"content_hash\":\"h\",\"graph\":\"g\",\"page_uuid\":\"p\"}"}})
meta (publish-common/parse-meta-header request)]
(is (= "h" (:content_hash meta)))
(is (= "g" (:graph meta)))
(is (= "p" (:page_uuid meta)))))
(testing "invalid json returns nil"
(let [request (js/Request. "https://publish.example/pages"
#js {:headers #js {"x-publish-meta" "{not-json"}})]
(is (nil? (publish-common/parse-meta-header request))))))
(deftest valid-meta-requires-core-fields
(is (some? (publish-common/valid-meta? {:content_hash "h" :graph "g" :page_uuid "p"})))
(is (nil? (publish-common/valid-meta? {:content_hash "h" :graph "g"})))
(is (nil? (publish-common/valid-meta? nil))))
(deftest get-sql-rows-handles-supported-shapes
(is (= [] (publish-common/get-sql-rows nil)))
(is (= [{"a" 1}]
(js->clj (publish-common/get-sql-rows #js {:rows #js [#js {"a" 1}]})
:keywordize-keys false)))
(let [row (js-obj)]
(aset row "rows" #js [#js {"b" 2}])
(is (= [{"rows" [{"b" 2}]}]
(js->clj (publish-common/get-sql-rows #js [row])
:keywordize-keys false)))))
(deftest json-error-response-helpers-set-status
(async done
(-> (p/let [unauthorized (publish-common/unauthorized)
forbidden (publish-common/forbidden)
bad-request (publish-common/bad-request "bad")
not-found (publish-common/not-found)
unauthorized-body (.json unauthorized)
forbidden-body (.json forbidden)
bad-request-body (.json bad-request)
not-found-body (.json not-found)]
(is (= 401 (.-status unauthorized)))
(is (= 403 (.-status forbidden)))
(is (= 400 (.-status bad-request)))
(is (= 404 (.-status not-found)))
(is (= "unauthorized" (aget unauthorized-body "error")))
(is (= "forbidden" (aget forbidden-body "error")))
(is (= "bad" (aget bad-request-body "error")))
(is (= "not found" (aget not-found-body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest normalize-etag-removes-double-quotes
(is (= "abc" (publish-common/normalize-etag "\"abc\"")))
(is (= "abc" (publish-common/normalize-etag "abc")))
(is (nil? (publish-common/normalize-etag nil))))
(deftest encode-path-encodes-path-segments
(is (= "with%20space/plus%2Bsign" (publish-common/encode-path "with space/plus+sign"))))
(deftest short-id-for-page-is-deterministic-and-fixed-length
(async done
(-> (p/let [a (publish-common/short-id-for-page "graph-1" "page-1")
b (publish-common/short-id-for-page "graph-1" "page-1")
c (publish-common/short-id-for-page "graph-1" "page-2")]
(is (= 10 (count a)))
(is (= a b))
(is (not= a c))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest hash-and-verify-password-roundtrip
(async done
(-> (p/let [hashed (publish-common/hash-password "secret-value")
ok? (publish-common/verify-password "secret-value" hashed)
wrong? (publish-common/verify-password "wrong-value" hashed)]
(is (string? hashed))
(is (true? ok?))
(is (false? wrong?))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest verify-password-rejects-invalid-hash-format
(is (false? (publish-common/verify-password "secret" "not-a-valid-hash"))))

View File

@@ -0,0 +1,27 @@
(ns logseq.publish.render-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.publish.render :as render]))
(deftest entity-properties-filters-explicitly-hidden-properties
(testing "property is filtered when ctx marks it hidden"
(let [entity {:user.property/secret "value"}
ctx {:property-hidden-by-ident {:user.property/secret true}}
result (render/entity-properties entity ctx {})]
(is (nil? (get result :user.property/secret))))))
(deftest entity-properties-filters-built-in-hidden-properties
(testing "built-in properties with :hide? true are filtered without property entity metadata"
(let [entity {:logseq.property/created-from-property "some-value"}
ctx {:property-hidden-by-ident {}}
result (render/entity-properties entity ctx {})]
(is (nil? (get result :logseq.property/created-from-property))))))
(deftest filter-tags-removes-built-in-tag-values
(testing "built-in class keyword tags are removed"
(let [result (render/filter-tags [:logseq.class/Tag :user.property/custom] {})]
(is (= [:user.property/custom] result))))
(testing "built-in class entities are removed"
(let [entities {1 {:db/ident :logseq.class/Tag}
2 {:db/ident :user.property/custom}}
result (render/filter-tags [1 2] entities)]
(is (= [2] result)))))

View File

@@ -0,0 +1,393 @@
(ns logseq.publish.routes-test
(:require [cljs.test :refer [async deftest is testing]]
[logseq.common.authorization :as authorization]
[logseq.publish.common :as publish-common]
[logseq.publish.routes :as routes]
[promesa.core :as p]))
(defn- json-response
[data]
(js/Response.
(js/JSON.stringify data)
#js {:status 200
:headers #js {"content-type" "application/json"}}))
(defn- short-url-env
[]
(let [do-stub #js {:fetch (fn [url _opts]
(cond
(= "https://publish/short/abc123" url)
(js/Promise.resolve
(json-response
#js {:page #js {"graph_uuid" "graph-1"
"page_uuid" "page-1"}}))
(re-matches #"https://publish/pages/[^/]+/[^/]+/password" url)
(js/Promise.resolve (json-response #js {}))
(re-matches #"https://publish/pages/[^/]+/[^/]+" url)
(js/Promise.resolve
(json-response
#js {"content_hash" "etag-1"
"r2_key" "publish/graph-1/page-1.transit"}))
:else
(js/Promise.resolve
(js/Response. "not found" #js {:status 404}))))}
do-ns #js {:idFromName (fn [_name] "index")
:get (fn [_id] do-stub)}
r2 #js {:get (fn [_key]
(js/Promise.resolve
#js {:arrayBuffer (fn []
(let [payload (.encode (js/TextEncoder.) "{}")]
(js/Promise.resolve (.-buffer payload))))}))}]
#js {"PUBLISH_META_DO" do-ns
"PUBLISH_R2" r2}))
(defn- empty-env []
#js {})
(defn- json-error-response
[status message]
(js/Response.
(js/JSON.stringify #js {"error" message})
#js {:status status
:headers #js {"content-type" "application/json"}}))
(defn- ok-json-response
[data]
(js/Response.
(js/JSON.stringify data)
#js {:status 200
:headers #js {"content-type" "application/json"}}))
(defn- method-from-opts
[opts]
(or (some-> opts (aget "method"))
"GET"))
(defn- permission-env
[route-dispatch]
(let [do-ns #js {:idFromName (fn [name] name)
:get (fn [id]
#js {:fetch (fn [url opts]
(js/Promise.resolve
(route-dispatch id url (method-from-opts opts))))})}]
#js {"PUBLISH_META_DO" do-ns}))
(deftest short-url-does-not-redirect-to-uuid-url
(testing "short URL should not redirect to /page/:graph/:page"
(async done
(let [request (js/Request. "https://publish.example/p/abc123?password=s3cr3t")
env (short-url-env)]
(-> (p/let [response (routes/handle-fetch request env)
body (.text response)]
(is (= 200 (.-status response)))
(is (nil? (.get (.-headers response) "location")))
(is (re-find #"<!doctype html>" body))
(is (not (re-find #"Page not found" body)))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))))
(deftest legacy-s-short-url-is-supported
(testing "legacy /s/:short-id should render page html"
(async done
(let [request (js/Request. "https://publish.example/s/abc123")
env (short-url-env)]
(-> (p/let [response (routes/handle-fetch request env)
body (.text response)]
(is (= 200 (.-status response)))
(is (nil? (.get (.-headers response) "location")))
(is (re-find #"<!doctype html>" body))
(is (not (re-find #"Page not found" body)))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))))
(deftest page-uuid-route-is-still-supported
(testing "legacy /page/:graph-uuid/:page-uuid should still render page html"
(async done
(let [request (js/Request. "https://publish.example/page/3bc00ad3-f421-41e7-8c65-40861c298be5/6954ee2a-506b-4dd9-bd6d-0dc24db9c055")
env (short-url-env)]
(-> (p/let [response (routes/handle-fetch request env)
body (.text response)]
(is (= 200 (.-status response)))
(is (nil? (.get (.-headers response) "location")))
(is (re-find #"<!doctype html>" body))
(is (not (re-find #"Page not found" body)))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))))
(deftest options-route-returns-cors-no-content
(async done
(let [request (js/Request. "https://publish.example/any"
#js {:method "OPTIONS"})]
(-> (p/let [response (routes/handle-fetch request (empty-env))]
(is (= 204 (.-status response)))
(is (= "*" (.get (.-headers response) "access-control-allow-origin")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done)))))))
(deftest home-route-renders-html
(async done
(let [request (js/Request. "https://publish.example/")]
(-> (p/let [response (routes/handle-fetch request (empty-env))
body (.text response)]
(is (= 200 (.-status response)))
(is (re-find #"<!doctype html>" body))
(is (re-find #"Logseq Publish" body))
(done))
(p/catch (fn [error]
(is nil (str error))
(done)))))))
(deftest static-assets-return-content-types
(async done
(-> (p/let [css-resp (routes/handle-fetch (js/Request. "https://publish.example/static/publish.css") (empty-env))
js-resp (routes/handle-fetch (js/Request. "https://publish.example/static/publish.js") (empty-env))
ext-resp (routes/handle-fetch (js/Request. "https://publish.example/static/tabler.ext.js") (empty-env))]
(is (= "text/css; charset=utf-8" (.get (.-headers css-resp) "content-type")))
(is (= "text/javascript; charset=utf-8" (.get (.-headers js-resp) "content-type")))
(is (= "text/javascript; charset=utf-8" (.get (.-headers ext-resp) "content-type")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest short-url-missing-id-returns-bad-request
(async done
(-> (p/let [response (routes/handle-fetch (js/Request. "https://publish.example/p/") (empty-env))
body (.json response)]
(is (= 400 (.-status response)))
(is (= "missing short id" (aget body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest short-url-not-found-returns-not-found
(async done
(let [do-stub #js {:fetch (fn [_url _opts]
(js/Promise.resolve (js/Response. "nope" #js {:status 404})))}
do-ns #js {:idFromName (fn [_name] "index")
:get (fn [_id] do-stub)}
env #js {"PUBLISH_META_DO" do-ns}]
(-> (p/let [response (routes/handle-fetch (js/Request. "https://publish.example/p/abc") env)
body (.json response)]
(is (= 404 (.-status response)))
(is (= "not found" (aget body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done)))))))
(deftest asset-route-validates-missing-or-invalid-id
(async done
(-> (p/let [missing-id-resp (routes/handle-fetch (js/Request. "https://publish.example/asset//") (empty-env))
missing-id-body (.json missing-id-resp)
invalid-id-resp (routes/handle-fetch (js/Request. "https://publish.example/asset/g/noext") (empty-env))
invalid-id-body (.json invalid-id-resp)]
(is (= 400 (.-status missing-id-resp)))
(is (= "missing asset id" (aget missing-id-body "error")))
(is (= 400 (.-status invalid-id-resp)))
(is (= "invalid asset id" (aget invalid-id-body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest user-route-validates-missing-username
(async done
(-> (p/let [response (routes/handle-fetch (js/Request. "https://publish.example/u/") (empty-env))
body (.json response)]
(is (= 400 (.-status response)))
(is (= "missing username" (aget body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest post-pages-without-auth-is-unauthorized
(async done
(-> (p/let [request (js/Request. "https://publish.example/pages"
#js {:method "POST"
:headers #js {"content-type" "application/transit+json"}
:body "{}"})
response (routes/handle-fetch request (empty-env))
body (.json response)]
(is (= 401 (.-status response)))
(is (= "unauthorized" (aget body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest delete-page-without-auth-is-unauthorized
(async done
(-> (p/let [request (js/Request. "https://publish.example/pages/graph-1/page-1"
#js {:method "DELETE"})
response (routes/handle-fetch request (empty-env))
body (.json response)]
(is (= 401 (.-status response)))
(is (= "unauthorized" (aget body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done))))))
(deftest delete-page-owner-mismatch-is-forbidden
(async done
(let [env (permission-env
(fn [id url method]
(cond
(and (= id "index")
(= method "GET")
(= url "https://publish/pages/graph-1/page-1"))
(ok-json-response #js {"owner_sub" "owner-a"})
:else
(json-error-response 404 "not found"))))
request (js/Request. "https://publish.example/pages/graph-1/page-1"
#js {:method "DELETE"
:headers #js {"authorization" "Bearer token"}})]
(-> (p/let [response (p/with-redefs [authorization/verify-jwt (fn [_ _] #js {"sub" "owner-b"})]
(routes/handle-fetch request env))
body (.json response)]
(is (= 403 (.-status response)))
(is (= "forbidden" (aget body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done)))))))
(deftest delete-page-owner-match-succeeds
(async done
(let [env (permission-env
(fn [id url method]
(cond
(and (= id "index")
(= method "GET")
(= url "https://publish/pages/graph-1/page-1"))
(ok-json-response #js {"owner_sub" "owner-a"})
(and (= id "index")
(= method "DELETE")
(= url "https://publish/pages/graph-1/page-1"))
(ok-json-response #js {"ok" true})
(and (= id "graph-1:page-1")
(= method "DELETE")
(= url "https://publish/pages/graph-1/page-1"))
(ok-json-response #js {"ok" true})
:else
(json-error-response 404 "not found"))))
request (js/Request. "https://publish.example/pages/graph-1/page-1"
#js {:method "DELETE"
:headers #js {"authorization" "Bearer token"}})]
(-> (p/let [response (p/with-redefs [authorization/verify-jwt (fn [_ _] #js {"sub" "owner-a"})]
(routes/handle-fetch request env))
body (.json response)]
(is (= 200 (.-status response)))
(is (true? (aget body "ok")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done)))))))
(deftest delete-graph-owner-mismatch-is-forbidden
(async done
(let [env (permission-env
(fn [id url method]
(cond
(and (= id "index")
(= method "GET")
(= url "https://publish/pages/graph-1"))
(ok-json-response #js {"pages" #js [#js {"owner_sub" "owner-a" "page_uuid" "page-1"}]})
:else
(json-error-response 404 "not found"))))
request (js/Request. "https://publish.example/pages/graph-1"
#js {:method "DELETE"
:headers #js {"authorization" "Bearer token"}})]
(-> (p/let [response (p/with-redefs [authorization/verify-jwt (fn [_ _] #js {"sub" "owner-b"})]
(routes/handle-fetch request env))
body (.json response)]
(is (= 403 (.-status response)))
(is (= "forbidden" (aget body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done)))))))
(deftest delete-graph-owner-match-succeeds
(async done
(let [env (permission-env
(fn [id url method]
(cond
(and (= id "index")
(= method "GET")
(= url "https://publish/pages/graph-1"))
(ok-json-response #js {"pages" #js [#js {"owner_sub" "owner-a" "page_uuid" "page-1"}
#js {"owner_sub" "owner-a" "page_uuid" "page-2"}]})
(and (= id "index")
(= method "DELETE")
(= url "https://publish/pages/graph-1"))
(ok-json-response #js {"ok" true})
(and (= method "DELETE")
(or (and (= id "graph-1:page-1") (= url "https://publish/pages/graph-1/page-1"))
(and (= id "graph-1:page-2") (= url "https://publish/pages/graph-1/page-2"))))
(ok-json-response #js {"ok" true})
:else
(json-error-response 404 "not found"))))
request (js/Request. "https://publish.example/pages/graph-1"
#js {:method "DELETE"
:headers #js {"authorization" "Bearer token"}})]
(-> (p/let [response (p/with-redefs [authorization/verify-jwt (fn [_ _] #js {"sub" "owner-a"})]
(routes/handle-fetch request env))
body (.json response)]
(is (= 200 (.-status response)))
(is (true? (aget body "ok")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done)))))))
(deftest get-page-requires-correct-password-when-protected
(async done
(let [env (permission-env
(fn [id url method]
(cond
(and (= id "graph-1:page-1")
(= method "GET")
(= url "https://publish/pages/graph-1/page-1"))
(ok-json-response #js {"content_hash" "h-1"
"r2_key" "publish/graph-1/page-1.transit"})
(and (= id "index")
(= method "GET")
(= url "https://publish/pages/graph-1/page-1/password"))
(ok-json-response #js {"password_hash" "pbkdf2$sha256$90000$x$y"})
:else
(json-error-response 404 "not found"))))
request (js/Request. "https://publish.example/pages/graph-1/page-1")]
(-> (p/let [response (p/with-redefs [publish-common/verify-password (fn [_ _] (p/resolved false))]
(routes/handle-fetch request env))
body (.json response)]
(is (= 401 (.-status response)))
(is (= "password required" (aget body "error")))
(done))
(p/catch (fn [error]
(is nil (str error))
(done)))))))

View File

@@ -0,0 +1,23 @@
(ns logseq.publish.test-runner
(:require [cljs.test :as ct]
[logseq.publish.common-test]
[logseq.publish.render-test]
[logseq.publish.routes-test]
[shadow.test :as st]
[shadow.test.env :as env]))
(derive ::node ::ct/default)
(defmethod ct/report [::node :end-run-tests] [m]
(if (ct/successful? m)
(js/process.exit 0)
(js/process.exit 1)))
(defn ^:dev/after-load reset-test-data! []
(-> (env/get-test-data)
(env/reset-test-data!)))
(defn main [& _args]
(reset-test-data!)
(let [test-env (ct/empty-env ::node)]
(st/run-all-tests test-env nil)))

View File

@@ -32,8 +32,6 @@ metadata in a Durable Object backed by SQLite.
- Deletes a published page
- `DELETE /pages/:graph-uuid`
- Deletes all pages for a graph
- `GET /pages`
- Lists metadata entries (from the index DO)
- `GET /tag/:tag-name`
- List all pages tagged with `:tag-name`
- `GET /ref/:page-name`