@tequila99/githuman 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/cli/config.js +13 -1
- package/dist/cli/server-runtime.js +3 -0
- package/dist/server/app.js +37 -4
- package/dist/server/config/agents.js +2 -1
- package/dist/server/config/openapi.js +4 -0
- package/dist/server/db/migrations.js +61 -0
- package/dist/server/errors/note-assets.js +3 -0
- package/dist/server/errors/notes.js +3 -0
- package/dist/server/hooks/notes/same-origin.js +37 -0
- package/dist/server/hooks/terminal/local-origin.js +2 -1
- package/dist/server/repositories/notes-catalog.repo.js +129 -0
- package/dist/server/repositories/notes.repo.js +444 -0
- package/dist/server/routes/agent.js +1 -0
- package/dist/server/routes/notes.js +213 -0
- package/dist/server/routes/terminal.js +2 -1
- package/dist/server/services/agent/context-builder.js +13 -0
- package/dist/server/services/git.service.js +15 -11
- package/dist/server/services/notes-external-assets.service.js +177 -0
- package/dist/server/services/notes-identity.service.js +811 -0
- package/dist/server/services/notes-operations.service.js +296 -0
- package/dist/server/services/notes-watcher.service.js +44 -0
- package/dist/server/services/notes-writer.service.js +116 -0
- package/dist/server/services/notes.service.js +150 -0
- package/dist/server/services/terminal/backend.js +6 -3
- package/dist/server/services/terminal/channel.js +6 -3
- package/dist/server/services/terminal/registry.js +10 -5
- package/dist/server/services/terminal/session.js +10 -5
- package/dist/server/services/terminal/tokens.js +4 -2
- package/dist/server/utils/hash.js +4 -0
- package/dist/server/utils/note-asset-format.js +19 -0
- package/dist/server/utils/remote-asset-request.js +193 -0
- package/dist/server/utils/request-abort.js +20 -0
- package/dist/server/utils/system-error.js +14 -0
- package/dist/shared/agents/schemas.js +14 -0
- package/dist/shared/events/types.js +1 -0
- package/dist/shared/notes/asset-formats.js +57 -0
- package/dist/shared/notes/constants.js +7 -0
- package/dist/shared/notes/document.js +118 -0
- package/dist/shared/notes/external-assets.js +153 -0
- package/dist/shared/notes/identity.js +24 -0
- package/dist/shared/notes/links.js +44 -0
- package/dist/shared/notes/note-path.js +17 -0
- package/dist/shared/notes/reference-path.js +55 -0
- package/dist/shared/notes/references.js +190 -0
- package/dist/shared/notes/schemas.js +222 -0
- package/dist/shared/notes/types.js +1 -0
- package/dist/shared/utils/heading-slug.js +7 -0
- package/dist/shared/utils/is-uuid.js +5 -0
- package/dist/shared/utils/safe-decode.js +9 -0
- package/dist/shared/utils/web-url.js +15 -0
- package/dist/web/assets/AppPage-DdmqiV2R.js +1 -0
- package/dist/web/assets/ClosePopup-lpH_1Ju2.js +1 -0
- package/dist/web/assets/CreateButton-Dnpwobd9.js +93 -0
- package/dist/web/assets/CreateReviewFabButton-KvKPEJzI.js +1 -0
- package/dist/web/assets/{ErrorNotFound-CYOZg8yN.js → ErrorNotFound-Cq0wNQMu.js} +1 -1
- package/dist/web/assets/IndexPage-DE-IkAhK.js +1 -0
- package/dist/web/assets/IndexPage-F8MMAAkn.css +1 -0
- package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-gV2CO0n9.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web/assets/MainLayout-DM30ynlR.css +1 -0
- package/dist/web/assets/MainLayout-wWA8ckBg.js +6 -0
- package/dist/web/assets/NoteVisualEditor-Bj13_oEl.js +1054 -0
- package/dist/web/assets/NoteVisualEditor-IH3s3d3W.css +1 -0
- package/dist/web/assets/NotesPage-FidBg83M.js +4 -0
- package/dist/web/assets/NotesPage-KaCEnfdz.css +1 -0
- package/dist/web/assets/PanelResize-C51WwwLD.css +1 -0
- package/dist/web/assets/PanelResize-DY0Nzmql.js +1 -0
- package/dist/web/assets/{PdfPreview-CIiKnzQN.js → PdfPreview-C7HomxEg.js} +1 -1
- package/dist/web/assets/QBtn-DoggJe1Q.js +1 -0
- package/dist/web/assets/QFab-C-YZWgmW.js +1 -0
- package/dist/web/assets/QForm-DpEhYVCa.js +4 -0
- package/dist/web/assets/QInput-DJgTh2Qy.js +1 -0
- package/dist/web/assets/QResizeObserver-DeRFR_WF.js +1 -0
- package/dist/web/assets/QScrollArea-BSSZoaV5.js +1 -0
- package/dist/web/assets/QSelect-2iOBkp1K.js +1 -0
- package/dist/web/assets/QSpace-B5NneHAd.js +1 -0
- package/dist/web/assets/QVirtualScroll-CZJMBpmR.js +1 -0
- package/dist/web/assets/ReviewDetailPage-BI1iAhSf.js +1 -0
- package/dist/web/assets/ReviewsPage-RyST0FlH.js +1 -0
- package/dist/web/assets/{StatusSwitcher-DTD5inU1.css → StatusSwitcher-BCv_9MtI.css} +1 -1
- package/dist/web/assets/StatusSwitcher-DAeNzNP7.js +1 -0
- package/dist/web/assets/{TerminalView-Ch-ndYr6.js → TerminalView-BAzJgUTq.js} +2 -2
- package/dist/web/assets/{TerminalView-BurzSD2L.css → TerminalView-C1jFlfLH.css} +1 -1
- package/dist/web/assets/_baseMerge-BzZFQ6mI.js +1 -0
- package/dist/web/assets/{abnfDiagram-O67JEVCF-Cab71FlB.js → abnfDiagram-O67JEVCF-BNZQ7duT.js} +1 -1
- package/dist/web/assets/agent-store-DP6Vrw6L.js +113 -0
- package/dist/web/assets/apl-gfMMiTNf.js +1 -0
- package/dist/web/assets/app-info-store-u6ZVFjv5.js +1 -0
- package/dist/web/assets/architecture-7GRP2DOG-BSdnjp9Z.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-Dl1WKOEi.js → architectureDiagram-NJMV4G6O-Bkj0kfFn.js} +1 -1
- package/dist/web/assets/asciiarmor-C_eisQCp.js +1 -0
- package/dist/web/assets/asn1-DCl_8MCW.js +1 -0
- package/dist/web/assets/asterisk-BEn0Cwpx.js +1 -0
- package/dist/web/assets/{blockDiagram-BEXU5L5S-CZ3Lxybj.js → blockDiagram-BEXU5L5S-CXO3zN2c.js} +1 -1
- package/dist/web/assets/brainfuck-D5EjA2JK.js +1 -0
- package/dist/web/assets/{c4Diagram-YGBWAQC7-ULi06PEd.js → c4Diagram-YGBWAQC7-BHfMeZNf.js} +1 -1
- package/dist/web/assets/ccount-CcG1m-Yh.js +1 -0
- package/dist/web/assets/channel-DmoOM6AB.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-B1f60wx3.js → chunk-3FUC2YCW-z1WbH7Gi.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-f6Pcl3dF.js → chunk-5DYCD2WN-K9HPch97.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-szX1QirW.js → chunk-742MDFTN-Bhjp9fC-.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-CP_ynmAQ.js → chunk-7INBJB4K-Do0EmU14.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-DPL74uZE.js → chunk-7M6MHVWA-veeEjsOh.js} +5 -5
- package/dist/web/assets/{chunk-7PRAP22T-DH9TawZY.js → chunk-7PRAP22T-CPqB5tCK.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-CKAuGvnf.js → chunk-GTNCS2PH-Civ1V52P.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-DTbHCHSN.js → chunk-GWA4HPMP-C7LzAlyZ.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-ClgvJMtC.js → chunk-MBY4JIJT-DfmJy0o_.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-Dq5Z7z8S.js → chunk-NETBCI7D-BZIKmMvL.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-C0By5mcm.js → chunk-O7XYJQB3-DcHeu6b7.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-BtquYpxe.js → chunk-UA2S7LBM-0SR2og6x.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-BlZL42XJ.js → chunk-WEXAMYUT-CJMSoM-Z.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-BbtQbAvv.js → chunk-Z7XXMR3K-q6fce66x.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-tC_YdRrm.js → chunk-ZIGJFQKS-DsPzen4Y.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-CvlA_A4N.js → classDiagram-v2-NBCMYWYE-Cpm_bvzb.js} +4 -4
- package/dist/web/assets/clike-D_6L3tnO.js +1 -0
- package/dist/web/assets/clojure-BflJGmDX.js +1 -0
- package/dist/web/assets/cmake-84nbDH0I.js +1 -0
- package/dist/web/assets/cobol-DHLQUbN7.js +1 -0
- package/dist/web/assets/coffeescript-De_zI4J0.js +1 -0
- package/dist/web/assets/commonlisp-DgAKBe0r.js +1 -0
- package/dist/web/assets/core-Bumj8M77.js +12 -0
- package/dist/web/assets/crystal-Bsdphhtm.js +1 -0
- package/dist/web/assets/css-CnaYoQib.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-DFbhKkIi.js → cynefin-OW5HDTMX-ZTr5XZMW.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-DHGaLNQN.js → cynefinDiagram-VND7K2PF-ByUmUx58.js} +1 -1
- package/dist/web/assets/cypher-p9eYesGn.js +1 -0
- package/dist/web/assets/d-DWkQyNnU.js +1 -0
- package/dist/web/assets/dagre-6A5THRUB-mQOy0E3r.js +4 -0
- package/dist/web/assets/{diagram-22UHCM2B-Djjo5ftL.js → diagram-22UHCM2B-BW0LPtyk.js} +8 -8
- package/dist/web/assets/{diagram-3UASUU5V-BcP7pJru.js → diagram-3UASUU5V-jo148ec5.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-o_vsc8em.js → diagram-ATOU4E4O-CJt4F0i4.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-eyN9Aru8.js → diagram-CDSNMT55-DEShJv56.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-Dfjipnvb.js → diagram-MLGK6HIB-DOFOADfs.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-C2chocRe.js → diagram-MPIPVDR6-ux537BMl.js} +1 -1
- package/dist/web/assets/diff-ChtP43wD.js +1 -0
- package/dist/web/assets/dist-3B0OaUZ-.js +1 -0
- package/dist/web/assets/dist-7xWvOKEv.js +1 -0
- package/dist/web/assets/dist-BThMRW5U.js +23 -0
- package/dist/web/assets/dist-BWa3ecCV.js +2 -0
- package/dist/web/assets/dist-BY24_62d.js +1 -0
- package/dist/web/assets/dist-BbdrgL5V.js +11 -0
- package/dist/web/assets/dist-Bo7anpPj.js +9 -0
- package/dist/web/assets/dist-Bu9yRqTU.js +1 -0
- package/dist/web/assets/dist-Bvnostf5.js +1 -0
- package/dist/web/assets/dist-C3sBgvRy.js +1 -0
- package/dist/web/assets/dist-CK1va5C_.js +1 -0
- package/dist/web/assets/dist-CLv-grWO.js +1 -0
- package/dist/web/assets/dist-CqGqStPp.js +1 -0
- package/dist/web/assets/dist-D1EUrS6y.js +6 -0
- package/dist/web/assets/dist-D2Ce45aH.js +13 -0
- package/dist/web/assets/dist-D7Llw4iH.js +1 -0
- package/dist/web/assets/dist-DL0dBIkt.js +1 -0
- package/dist/web/assets/dist-DUt0feNB.js +1 -0
- package/dist/web/assets/dist-DZQ2IsS9.js +1 -0
- package/dist/web/assets/dist-DpKJcdvz.js +1 -0
- package/dist/web/assets/dist-DuZv0LJj.js +1 -0
- package/dist/web/assets/dist-LFPPEX39.js +1 -0
- package/dist/web/assets/dist-lC0EKTFH.js +1 -0
- package/dist/web/assets/dockerfile-DJkHQgkl.js +1 -0
- package/dist/web/assets/dtd-DTF72YNO.js +1 -0
- package/dist/web/assets/dylan-BCDoL_-p.js +1 -0
- package/dist/web/assets/ebnf-D4c0_ac3.js +1 -0
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-CE58dwC-.js → ebnfDiagram-ZINNZB2B-DEybuZle.js} +1 -1
- package/dist/web/assets/ecl-BVIeWmwY.js +1 -0
- package/dist/web/assets/eiffel-BEjRio4Q.js +1 -0
- package/dist/web/assets/{elk-276RUBZZ-KyTn9aJi.js → elk-276RUBZZ-CZXZRpGb.js} +1 -1
- package/dist/web/assets/elm-Cshzl8qu.js +1 -0
- package/dist/web/assets/{erDiagram-OPXOYQCR-D9aOi7ZD.js → erDiagram-OPXOYQCR-BgRqGAt5.js} +1 -1
- package/dist/web/assets/erlang-BV0c4Hdn.js +1 -0
- package/dist/web/assets/eventmodeling-NTZA5JFV-ZuRq6Sae.js +1 -0
- package/dist/web/assets/factor-LKH5gN8L.js +1 -0
- package/dist/web/assets/fcl-ClOhbFR7.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-DRyHAdLb.js +1 -0
- package/dist/web/assets/forth-D6RzAl64.js +1 -0
- package/dist/web/assets/fortran-Bt6PBEDR.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-DRAI6xWM.js → ganttDiagram-FUAMR5RP-CBJwX1gk.js} +1 -1
- package/dist/web/assets/gas-BHEdbvp9.js +1 -0
- package/dist/web/assets/gherkin-oBAE_ms0.js +1 -0
- package/dist/web/assets/{gitGraph-4MIJSDKK-D1lAB6tr.js → gitGraph-4MIJSDKK-Dw-XA3G1.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-77GVnwya.js → gitGraphDiagram-X574FWY7-BpGccoWp.js} +1 -1
- package/dist/web/assets/graphlib-C64HifUL.js +1 -0
- package/dist/web/assets/groovy-CUpZ7dEl.js +1 -0
- package/dist/web/assets/haskell-BRsoo5mP.js +1 -0
- package/dist/web/assets/haxe-DjVOVvNP.js +1 -0
- package/dist/web/assets/http-CLVgA2GD.js +1 -0
- package/dist/web/assets/i18n-sfJvmpVb.js +1 -0
- package/dist/web/assets/idl-BUZw3wgd.js +1 -0
- package/dist/web/assets/index-B2CzafCr.js +2 -0
- package/dist/web/assets/{info-A6RAGUB7-Di1qRpwO.js → info-A6RAGUB7-Dj5RfJsg.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-gceHneJ0.js → infoDiagram-VRGFBTTK-DyVSH991.js} +1 -1
- package/dist/web/assets/{init-app-IHgDyoRH.js → init-app-DSS44aRI.js} +1 -1
- package/dist/web/assets/{is-DssfAlMe.js → is-CE4jkrGQ.js} +1 -1
- package/dist/web/assets/isArrayLikeObject-BbaDyxId.js +1 -0
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-B9QDQoSG.js → ishikawaDiagram-OU5B5YK6-Cu0n2oqT.js} +1 -1
- package/dist/web/assets/javascript-Bu8mWwtz.js +1 -0
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-CVTMW0pg.js → journeyDiagram-ZHPQQLJL-Bz1Ot6Zg.js} +1 -1
- package/dist/web/assets/julia-CQfgDRfP.js +1 -0
- package/dist/web/assets/{kanban-definition-PNTS6WVX-TNoWYrUr.js → kanban-definition-PNTS6WVX-C42vuY7H.js} +1 -1
- package/dist/web/assets/{line-B_1CHIjZ.js → line-C5WKurbS.js} +1 -1
- package/dist/web/assets/livescript-BXxG0Yva.js +1 -0
- package/dist/web/assets/lua-DPCEkfD_.js +1 -0
- package/dist/web/assets/markdown-DBTEhbhQ.js +15 -0
- package/dist/web/assets/mathematica-u8YMmbU0.js +1 -0
- package/dist/web/assets/mbox-BEMVcqjs.js +1 -0
- package/dist/web/assets/{mermaid-parser.core-LAeoGxkL.js → mermaid-parser.core-B7l8qh8B.js} +3 -3
- package/dist/web/assets/{mermaid.core-DMppSzfM.js → mermaid.core-Drs2mjGg.js} +7 -7
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-DJ-6nTWg.js → mindmap-definition-NLK3R4M7-BV1FNwCn.js} +1 -1
- package/dist/web/assets/mirc-C9zpzAZU.js +1 -0
- package/dist/web/assets/mllike-B45xgp2S.js +1 -0
- package/dist/web/assets/modelica-CqHI_q-D.js +1 -0
- package/dist/web/assets/mscgen-qgSLujhx.js +1 -0
- package/dist/web/assets/mumps-BSzj6Xyz.js +1 -0
- package/dist/web/assets/nginx-8H7HVH4w.js +1 -0
- package/dist/web/assets/nsis-viPvXFiq.js +1 -0
- package/dist/web/assets/ntriples-B9h1VB_g.js +1 -0
- package/dist/web/assets/octave-D2Q8cUp1.js +1 -0
- package/dist/web/assets/oz-BwTct_5T.js +1 -0
- package/dist/web/assets/{packet-AYTQ26CC-B4tvN4nf.js → packet-AYTQ26CC-BPmUY1Ig.js} +1 -1
- package/dist/web/assets/pascal-Dp_KdFgX.js +1 -0
- package/dist/web/assets/{pegDiagram-GJSIUBJH-BwQ6jaVw.js → pegDiagram-GJSIUBJH-D6NPxPdr.js} +1 -1
- package/dist/web/assets/perl-BkSkiI9m.js +1 -0
- package/dist/web/assets/{pie-WAS4IAKB-ZwlyCxls.js → pie-WAS4IAKB-CTp2jNAc.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-BDYLBnB1.js → pieDiagram-5QR66LMP-BVkABrg7.js} +1 -1
- package/dist/web/assets/pig-AF4Hwhju.js +1 -0
- package/dist/web/assets/pinia-BVWaJ8NX.js +1 -0
- package/dist/web/assets/powershell-BcIto6Sf.js +1 -0
- package/dist/web/assets/properties-C357ku5U.js +1 -0
- package/dist/web/assets/protobuf-D1ij_kNL.js +1 -0
- package/dist/web/assets/pug-3K-JMW4g.js +1 -0
- package/dist/web/assets/puppet-BZyAS7I4.js +1 -0
- package/dist/web/assets/purify.es--06brOip.js +3 -0
- package/dist/web/assets/python-B_1Jd8HM.js +1 -0
- package/dist/web/assets/q-B9NhS7L_.js +1 -0
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-CkmLCxOG.js → quadrantDiagram-O4NWA36T-LZCjVd08.js} +1 -1
- package/dist/web/assets/r-CxDmU1I4.js +1 -0
- package/dist/web/assets/{radar-RG4KPBEZ-Ca7H5QOY.js → radar-RG4KPBEZ-CCiUrmsg.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-CX3Z2QYJ.js → railroad-74A4TZTK-CZn8VJbK.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-B-9tG9pW.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-D7lUyGOq.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-ja94t32K.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-D8ii6sM4.js → railroadDiagram-XR7U4H2S-nopy-mdw.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-iHY-tE7c.js → requirementDiagram-PLB6GJNP-BOeMzf-x.js} +1 -1
- package/dist/web/assets/rpm-gpDK5sbt.js +1 -0
- package/dist/web/assets/ruby-ChfNwlCY.js +1 -0
- package/dist/web/assets/{safe-storage-BCP757jk.js → safe-storage-aRoyeFxv.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-CBprSUZk.js → sankeyDiagram-IPEJSGJF-Bbk0AQUe.js} +1 -1
- package/dist/web/assets/sas-C2OyD2Y6.js +1 -0
- package/dist/web/assets/scheme-D9qYN0V5.js +1 -0
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-BkphpOQ_.js → sequenceDiagram-PO4LG4MO-ZpubjKFc.js} +1 -1
- package/dist/web/assets/shell-DwuoZtxw.js +1 -0
- package/dist/web/assets/sieve-tLWDfEkZ.js +1 -0
- package/dist/web/assets/simple-mode-DXiLM1-G.js +1 -0
- package/dist/web/assets/smalltalk-BLp5-jwC.js +1 -0
- package/dist/web/assets/solr-B78wSQJQ.js +1 -0
- package/dist/web/assets/sparql-Dz-mlCAC.js +1 -0
- package/dist/web/assets/spreadsheet-CTIncYxj.js +1 -0
- package/dist/web/assets/sql-DuQMFDvy.js +1 -0
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-HN1RwAPZ.js → stateDiagram-v2-GCMORJYK-DNjDh9yZ.js} +2 -2
- package/dist/web/assets/stex-DNzd62Q_.js +1 -0
- package/dist/web/assets/stylus-BOVgKnng.js +1 -0
- package/dist/web/assets/swift-DKB6_1j6.js +1 -0
- package/dist/web/assets/{swimlanes-2SLR337P-Cc8jyETz.js → swimlanes-2SLR337P-C-_UzZF_.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-CVSZNvS2.js +8 -0
- package/dist/web/assets/tcl-BGfruO1u.js +1 -0
- package/dist/web/assets/textile-C4jVoHG1.js +1 -0
- package/dist/web/assets/theme-CeQhGDjh.js +1 -0
- package/dist/web/assets/tiddlywiki-VlUiK86J.js +1 -0
- package/dist/web/assets/tiki-AK6FwT8q.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-Bas0Ksw6.js → timeline-definition-EJHVYXUP-BiaJyHdJ.js} +1 -1
- package/dist/web/assets/toml-BPTmHmyx.js +1 -0
- package/dist/web/assets/{treeView-Q6P3EWNA-Ck-xt3KO.js → treeView-Q6P3EWNA-CENjlerX.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-k9Ay9d-c.js → treemap-WGGIJYW6-CBpdf3L2.js} +1 -1
- package/dist/web/assets/troff-C2dAgh7P.js +1 -0
- package/dist/web/assets/ttcn-PPSTk1ow.js +1 -0
- package/dist/web/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
- package/dist/web/assets/turtle-Cx3rYX2g.js +1 -0
- package/dist/web/assets/use-add-agent-context-SApxn9fW.js +1 -0
- package/dist/web/assets/{use-app-theme-BfRqhHi_.js → use-app-theme-DP8Ymwwb.js} +1 -1
- package/dist/web/assets/use-dark-BHObfh0O.js +1 -0
- package/dist/web/assets/use-panel-width-CTXD_csP.js +1 -0
- package/dist/web/assets/use-router-link-BGc0noIO.js +1 -0
- package/dist/web/assets/use-server-events-C13Sw-rU.js +1 -0
- package/dist/web/assets/use-virtual-scroll-BSXYZjZ5.js +1 -0
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-BlzYgvXB.js → usecaseDiagram-POWQR4AR-CcJ83_Ve.js} +1 -1
- package/dist/web/assets/vb-BthdM_4N.js +1 -0
- package/dist/web/assets/vbscript-D9f6rSsU.js +1 -0
- package/dist/web/assets/velocity-CqftRsjg.js +1 -0
- package/dist/web/assets/{vennDiagram-UO4OBE2U-gjloIQdf.js → vennDiagram-UO4OBE2U-DBjzpbFr.js} +1 -1
- package/dist/web/assets/verilog-CjI9Y6RI.js +1 -0
- package/dist/web/assets/vhdl-kS471soi.js +1 -0
- package/dist/web/assets/{wardley-WFR3VGLG-BnsYd7B2.js → wardley-WFR3VGLG-DmlbUWUM.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-ktflnFLZ.js → wardleyDiagram-VNRHLVJA-CWSZCqf4.js} +1 -1
- package/dist/web/assets/webidl-D5jiJvOU.js +1 -0
- package/dist/web/assets/window-store-DNnKUhxs.js +1 -0
- package/dist/web/assets/xquery-BKJx4mf9.js +1 -0
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-Ch5MYgz0.js → xychartDiagram-PMCCYNJV-CMnNSaZv.js} +1 -1
- package/dist/web/assets/yacas-CXXgtw0p.js +1 -0
- package/dist/web/assets/z80-Co3PbHdE.js +1 -0
- package/dist/web/index.html +6 -3
- package/package.json +10 -2
- package/dist/web/assets/AppPage-BGcEFW_o.js +0 -93
- package/dist/web/assets/CreateReviewFabButton-CdUXVriU.js +0 -1
- package/dist/web/assets/IndexPage-CYFIGWy9.css +0 -1
- package/dist/web/assets/IndexPage-OqgW0yH8.js +0 -1
- package/dist/web/assets/MainLayout-Bi0JE3ER.js +0 -11
- package/dist/web/assets/MainLayout-DPkSlqMr.css +0 -1
- package/dist/web/assets/QBtn-CzrOO1zA.js +0 -1
- package/dist/web/assets/QScrollArea-C4BKutrt.js +0 -1
- package/dist/web/assets/QSelect-BXUaq4ky.js +0 -1
- package/dist/web/assets/QSpace-BMpRu3a-.js +0 -13
- package/dist/web/assets/ReviewDetailPage-DlR7Vxkw.js +0 -1
- package/dist/web/assets/ReviewsPage-DM8XtlUK.js +0 -1
- package/dist/web/assets/StatusSwitcher-Blg5JmAa.js +0 -1
- package/dist/web/assets/agent-store-CDuiBhST.js +0 -1
- package/dist/web/assets/app-info-store-BmD_P8q2.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-DMgDJZyQ.js +0 -1
- package/dist/web/assets/channel-BnUyOMnk.js +0 -1
- package/dist/web/assets/core-C37KF-j0.js +0 -12
- package/dist/web/assets/dagre-6A5THRUB-Bk3Po5o5.js +0 -4
- package/dist/web/assets/eventmodeling-NTZA5JFV-7YdTAmVS.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-eOIiY2oF.js +0 -1
- package/dist/web/assets/graphlib-DS17s2tU.js +0 -1
- package/dist/web/assets/i18n-C5Xnmkwe.js +0 -1
- package/dist/web/assets/index-BqedItFI.js +0 -2
- package/dist/web/assets/pinia-BKoxNmoW.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-DZSn8MK2.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-BVTJtDBq.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-DMzsNGzj.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BxXXHQ70.js +0 -8
- package/dist/web/assets/theme-CPoPdFci.js +0 -1
- package/dist/web/assets/use-panel-width-C9dWftsJ.js +0 -1
- package/dist/web/assets/use-server-events-znzHKZZx.js +0 -1
- package/dist/web/assets/use-virtual-scroll-CIwaJCUj.js +0 -1
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import PQueue from 'p-queue';
|
|
2
|
+
import { noteBasename, noteChildPath, noteParentPath } from "../../shared/notes/note-path.js";
|
|
3
|
+
import { lstat } from 'node:fs/promises';
|
|
4
|
+
import { isSystemError } from "../utils/system-error.js";
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { NotesCatalogRepository } from "../repositories/notes-catalog.repo.js";
|
|
7
|
+
import { NotesOperationsService } from "./notes-operations.service.js";
|
|
8
|
+
import { NotesWriter } from "./notes-writer.service.js";
|
|
9
|
+
import { noteIdentity, withNoteIdentity } from "../../shared/notes/identity.js";
|
|
10
|
+
import { noteMetadata } from "../../shared/notes/document.js";
|
|
11
|
+
import { noteReferences, referenceTarget, sameReference } from "../../shared/notes/references.js";
|
|
12
|
+
import { BadRequestError, NotFoundError } from "../errors/http.js";
|
|
13
|
+
import { NoteConflictError, NoteExistsError } from "../errors/notes.js";
|
|
14
|
+
import { sha256Hex } from "../utils/hash.js";
|
|
15
|
+
import { safeDecodeURIComponent } from "../../shared/utils/safe-decode.js";
|
|
16
|
+
// A second complete scan confirms absence, instead of treating an atomic save as deletion.
|
|
17
|
+
const ABSENCE_GRACE_MS = 2500;
|
|
18
|
+
// Longest file name that common file systems accept, in bytes.
|
|
19
|
+
const MAX_FILENAME_BYTES = 255;
|
|
20
|
+
function newRecord(id, path, version) {
|
|
21
|
+
const now = new Date().toISOString();
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
path,
|
|
25
|
+
version,
|
|
26
|
+
presence: 'present',
|
|
27
|
+
createdAt: now,
|
|
28
|
+
updatedAt: now,
|
|
29
|
+
deletedAt: null,
|
|
30
|
+
redirectId: null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function portablePath(path) {
|
|
34
|
+
for (const part of path.split('/')) {
|
|
35
|
+
if (/[<>:"\\|?*]/.test(part) ||
|
|
36
|
+
part.split('').some(char => char.charCodeAt(0) < 32) ||
|
|
37
|
+
/[. ]$/.test(part) ||
|
|
38
|
+
/^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(part)) {
|
|
39
|
+
throw new BadRequestError('The destination name is not portable to Windows.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Path a deleted note moves to. The suffix frees the original name for a new
|
|
45
|
+
* note. The stem is cut by bytes so the name fits the file system limit.
|
|
46
|
+
*/
|
|
47
|
+
function deletedNotePath(path, uuid) {
|
|
48
|
+
const suffix = `_deleted_${uuid}.md`;
|
|
49
|
+
const stem = noteBasename(path).replace(/\.md$/i, '');
|
|
50
|
+
const room = MAX_FILENAME_BYTES - Buffer.byteLength(suffix);
|
|
51
|
+
let cut = stem;
|
|
52
|
+
while (Buffer.byteLength(cut) > room) {
|
|
53
|
+
cut = cut.slice(0, -1);
|
|
54
|
+
}
|
|
55
|
+
return noteChildPath(noteParentPath(path), cut + suffix);
|
|
56
|
+
}
|
|
57
|
+
export class NotesIdentityService {
|
|
58
|
+
catalog;
|
|
59
|
+
writer;
|
|
60
|
+
operations;
|
|
61
|
+
// One action at a time: catalog and file changes must not interleave.
|
|
62
|
+
queue = new PQueue({ concurrency: 1 });
|
|
63
|
+
cache = new Map();
|
|
64
|
+
absentSince = new Map();
|
|
65
|
+
// Signature of each file that failed to read, so a poll does not read it again.
|
|
66
|
+
unreadable = new Map();
|
|
67
|
+
closed = false;
|
|
68
|
+
files;
|
|
69
|
+
constructor(files, db) {
|
|
70
|
+
this.files = files;
|
|
71
|
+
this.catalog = new NotesCatalogRepository(db, files.root);
|
|
72
|
+
this.writer = new NotesWriter(files.root);
|
|
73
|
+
this.operations = new NotesOperationsService({
|
|
74
|
+
files,
|
|
75
|
+
catalog: this.catalog,
|
|
76
|
+
writer: this.writer,
|
|
77
|
+
record: id => this.record(id),
|
|
78
|
+
changed: (id, note) => {
|
|
79
|
+
this.catalog.put({
|
|
80
|
+
...this.record(id),
|
|
81
|
+
version: note.version,
|
|
82
|
+
updatedAt: new Date().toISOString()
|
|
83
|
+
});
|
|
84
|
+
this.indexLinks(id, note.text);
|
|
85
|
+
this.cache.set(note.path, {
|
|
86
|
+
signature: '',
|
|
87
|
+
note: { version: note.version },
|
|
88
|
+
identity: noteIdentity(note.text),
|
|
89
|
+
metadata: noteMetadata(note.text)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
run(action) {
|
|
95
|
+
return this.queue.add(async () => {
|
|
96
|
+
if (this.closed)
|
|
97
|
+
throw new BadRequestError('Notes storage is closing.');
|
|
98
|
+
return action();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async close() {
|
|
102
|
+
this.closed = true;
|
|
103
|
+
await this.queue.onIdle();
|
|
104
|
+
await this.writer.close();
|
|
105
|
+
}
|
|
106
|
+
record(id) {
|
|
107
|
+
let note = this.catalog.get(id);
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
while (note?.redirectId && !seen.has(note.id)) {
|
|
110
|
+
seen.add(note.id);
|
|
111
|
+
note = this.catalog.get(note.redirectId);
|
|
112
|
+
}
|
|
113
|
+
if (!note)
|
|
114
|
+
throw new NotFoundError('Unknown note identity.');
|
|
115
|
+
return note;
|
|
116
|
+
}
|
|
117
|
+
issue(id, message, noteId, candidateId = null, kind = 'identity') {
|
|
118
|
+
this.catalog.issue({ id, message, noteId, candidateId, kind });
|
|
119
|
+
}
|
|
120
|
+
async synchronize() {
|
|
121
|
+
await this.files.path('probe.md');
|
|
122
|
+
try {
|
|
123
|
+
await lstat(this.files.root);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (isSystemError(error, 'ENOENT') && !this.catalog.all().length) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
throw new BadRequestError('Notes storage is unavailable. Existing identities are retained.');
|
|
130
|
+
}
|
|
131
|
+
await this.writer.acquire();
|
|
132
|
+
if (this.catalog.operations().length)
|
|
133
|
+
await this.operations.recover();
|
|
134
|
+
const entries = await this.files.list();
|
|
135
|
+
const documents = [];
|
|
136
|
+
// Read the whole snapshot successfully before changing any presence records.
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.folder)
|
|
139
|
+
continue;
|
|
140
|
+
let references;
|
|
141
|
+
let cached = this.cache.get(entry.path);
|
|
142
|
+
if (!cached || cached.signature !== entry.signature) {
|
|
143
|
+
if (this.unreadable.get(entry.path) === entry.signature)
|
|
144
|
+
continue;
|
|
145
|
+
let note;
|
|
146
|
+
try {
|
|
147
|
+
note = await this.files.read(entry.path);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// One bad file must not stop the synchronization of other notes.
|
|
151
|
+
if (!(error instanceof BadRequestError))
|
|
152
|
+
throw error;
|
|
153
|
+
this.unreadable.set(entry.path, entry.signature);
|
|
154
|
+
this.issue('unreadable:' + entry.path, 'Skipped ' + entry.path + ': ' + error.message, null, null, 'metadata');
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
this.unreadable.delete(entry.path);
|
|
158
|
+
this.catalog.dismiss('unreadable:' + entry.path);
|
|
159
|
+
cached = {
|
|
160
|
+
signature: entry.signature,
|
|
161
|
+
note: { version: note.version },
|
|
162
|
+
identity: noteIdentity(note.text),
|
|
163
|
+
metadata: noteMetadata(note.text)
|
|
164
|
+
};
|
|
165
|
+
references = noteReferences(note.text);
|
|
166
|
+
this.cache.set(entry.path, cached);
|
|
167
|
+
}
|
|
168
|
+
documents.push({
|
|
169
|
+
path: entry.path,
|
|
170
|
+
note: cached.note,
|
|
171
|
+
yamlId: cached.identity.id,
|
|
172
|
+
identityWarning: cached.identity.warning,
|
|
173
|
+
...(references ? { references } : {})
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const entryPaths = new Set(entries.map(entry => entry.path));
|
|
177
|
+
for (const path of this.unreadable.keys()) {
|
|
178
|
+
if (entryPaths.has(path))
|
|
179
|
+
continue;
|
|
180
|
+
this.unreadable.delete(path);
|
|
181
|
+
this.catalog.dismiss('unreadable:' + path);
|
|
182
|
+
}
|
|
183
|
+
const records = this.catalog.all();
|
|
184
|
+
const byId = new Map(records.map(record => [record.id, record]));
|
|
185
|
+
const presentByPath = new Map(records
|
|
186
|
+
.filter(record => (record.presence === 'present' || record.deletedAt) &&
|
|
187
|
+
!record.redirectId)
|
|
188
|
+
.map(record => [record.path, record]));
|
|
189
|
+
const livePaths = new Set(documents.map(doc => doc.path));
|
|
190
|
+
const counts = new Map();
|
|
191
|
+
for (const doc of documents) {
|
|
192
|
+
if (doc.yamlId)
|
|
193
|
+
counts.set(doc.yamlId, (counts.get(doc.yamlId) ?? 0) + 1);
|
|
194
|
+
}
|
|
195
|
+
const changes = [];
|
|
196
|
+
const changed = new Set();
|
|
197
|
+
this.catalog.transaction(() => {
|
|
198
|
+
// Release paths first, so swaps and folder moves do not violate uniqueness.
|
|
199
|
+
for (const record of records) {
|
|
200
|
+
if (record.presence !== 'present' || livePaths.has(record.path)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (documents.some(doc => doc.yamlId === record.id && counts.get(record.id) === 1)) {
|
|
204
|
+
this.catalog.put({ ...record, presence: 'missing' });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const since = this.absentSince.get(record.id);
|
|
208
|
+
if (since === undefined) {
|
|
209
|
+
this.catalog.reopen('missing:' + record.id);
|
|
210
|
+
this.absentSince.set(record.id, Date.now());
|
|
211
|
+
}
|
|
212
|
+
else if (Date.now() - since >= ABSENCE_GRACE_MS) {
|
|
213
|
+
this.catalog.put({ ...record, presence: 'missing' });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const doc of documents) {
|
|
217
|
+
const byPath = presentByPath.get(doc.path);
|
|
218
|
+
const hinted = doc.yamlId && counts.get(doc.yamlId) === 1
|
|
219
|
+
? byId.get(doc.yamlId)
|
|
220
|
+
: undefined;
|
|
221
|
+
let record = byPath;
|
|
222
|
+
if (hinted && hinted !== byPath && !livePaths.has(hinted.path)) {
|
|
223
|
+
if (byPath)
|
|
224
|
+
this.catalog.put({ ...byPath, presence: 'missing' });
|
|
225
|
+
record = hinted;
|
|
226
|
+
}
|
|
227
|
+
else if (!record && hinted && hinted.path === doc.path) {
|
|
228
|
+
record = hinted;
|
|
229
|
+
}
|
|
230
|
+
if (!record) {
|
|
231
|
+
const id = doc.yamlId &&
|
|
232
|
+
counts.get(doc.yamlId) === 1 &&
|
|
233
|
+
!this.catalog.get(doc.yamlId)
|
|
234
|
+
? doc.yamlId
|
|
235
|
+
: randomUUID();
|
|
236
|
+
record = newRecord(id, doc.path, doc.note.version);
|
|
237
|
+
this.catalog.put(record);
|
|
238
|
+
changed.add(record.id);
|
|
239
|
+
}
|
|
240
|
+
if (record.path !== doc.path) {
|
|
241
|
+
changes.push({ id: record.id, from: record.path, to: doc.path });
|
|
242
|
+
}
|
|
243
|
+
if (record.version !== doc.note.version)
|
|
244
|
+
changed.add(record.id);
|
|
245
|
+
if (record.path !== doc.path ||
|
|
246
|
+
record.presence !== 'present' ||
|
|
247
|
+
record.version !== doc.note.version) {
|
|
248
|
+
this.catalog.put({
|
|
249
|
+
...record,
|
|
250
|
+
path: doc.path,
|
|
251
|
+
presence: 'present',
|
|
252
|
+
version: doc.note.version,
|
|
253
|
+
updatedAt: record.version === doc.note.version
|
|
254
|
+
? record.updatedAt
|
|
255
|
+
: new Date().toISOString()
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
this.absentSince.delete(record.id);
|
|
259
|
+
this.catalog.dismiss('missing:' + record.id);
|
|
260
|
+
const identity = { id: doc.yamlId, warning: doc.identityWarning };
|
|
261
|
+
const warning = identity.warning ??
|
|
262
|
+
(identity.id && identity.id !== record.id
|
|
263
|
+
? 'The YAML ID conflicts with this note or is duplicated. The catalog identity is retained.'
|
|
264
|
+
: null);
|
|
265
|
+
if (warning && !record.deletedAt) {
|
|
266
|
+
this.issue('metadata:' + record.id, warning, record.id, null, 'metadata');
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
this.catalog.dismiss('metadata:' + record.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
if (changes.length) {
|
|
274
|
+
const operation = await this.operations.prepare(changes, '', '', false, true);
|
|
275
|
+
await this.operations.execute(operation);
|
|
276
|
+
}
|
|
277
|
+
const current = this.catalog.all();
|
|
278
|
+
const documentsByPath = new Map(documents.map(doc => [doc.path, doc]));
|
|
279
|
+
const currentByPath = new Map(current
|
|
280
|
+
.filter(note => note.presence === 'present' && !note.redirectId)
|
|
281
|
+
.map(note => [note.path, note]));
|
|
282
|
+
const byVersion = new Map();
|
|
283
|
+
for (const candidate of current.filter(note => note.presence === 'present' && !note.redirectId)) {
|
|
284
|
+
const group = byVersion.get(candidate.version) ?? [];
|
|
285
|
+
group.push(candidate);
|
|
286
|
+
byVersion.set(candidate.version, group);
|
|
287
|
+
}
|
|
288
|
+
for (const old of current.filter(note => note.presence === 'missing' && !note.redirectId && !note.deletedAt)) {
|
|
289
|
+
const samePath = currentByPath.get(old.path);
|
|
290
|
+
const matches = (byVersion.get(old.version) ?? []).filter(candidate => !documentsByPath.get(candidate.path)?.yamlId);
|
|
291
|
+
const candidate = samePath ?? (matches.length === 1 ? matches[0] : undefined);
|
|
292
|
+
this.issue('missing:' + old.id, 'File missing: ' +
|
|
293
|
+
old.path +
|
|
294
|
+
'. Choose a moved file, or confirm deletion.', old.id, candidate?.id ?? null);
|
|
295
|
+
}
|
|
296
|
+
for (const doc of documents) {
|
|
297
|
+
const record = currentByPath.get(doc.path);
|
|
298
|
+
if (!record || record.deletedAt || !changed.has(record.id))
|
|
299
|
+
continue;
|
|
300
|
+
const references = changes.length
|
|
301
|
+
? noteReferences((await this.files.read(doc.path)).text)
|
|
302
|
+
: (doc.references ?? []);
|
|
303
|
+
this.indexReferences(record.id, references, current);
|
|
304
|
+
}
|
|
305
|
+
for (const path of this.cache.keys()) {
|
|
306
|
+
if (!livePaths.has(path))
|
|
307
|
+
this.cache.delete(path);
|
|
308
|
+
}
|
|
309
|
+
// Unreadable files stay visible through their issue, not as openable entries.
|
|
310
|
+
const hiddenPaths = new Set(current.filter(note => note.deletedAt).map(note => note.path));
|
|
311
|
+
return entries.filter(entry => !hiddenPaths.has(entry.path) &&
|
|
312
|
+
this.unreadable.get(entry.path) !== entry.signature);
|
|
313
|
+
}
|
|
314
|
+
indexLinks(id, text, snapshot) {
|
|
315
|
+
this.indexReferences(id, noteReferences(text), snapshot);
|
|
316
|
+
}
|
|
317
|
+
indexReferences(id, refs, snapshot) {
|
|
318
|
+
const record = this.record(id);
|
|
319
|
+
const oldLinks = this.catalog.links(id);
|
|
320
|
+
if (!refs.length) {
|
|
321
|
+
if (oldLinks.length)
|
|
322
|
+
this.catalog.setLinks(id, []);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const records = (snapshot ?? this.catalog.all()).filter(note => !note.redirectId);
|
|
326
|
+
const paths = records
|
|
327
|
+
.filter(note => note.presence === 'present')
|
|
328
|
+
.map(note => note.path);
|
|
329
|
+
const links = refs
|
|
330
|
+
.filter(ref => !ref.image)
|
|
331
|
+
.map(ref => {
|
|
332
|
+
const target = referenceTarget(ref, record.path, paths);
|
|
333
|
+
const old = oldLinks.filter(link => sameReference(link.reference, ref));
|
|
334
|
+
const duplicates = refs.filter(other => sameReference(ref, other)).length;
|
|
335
|
+
if (old.length === duplicates &&
|
|
336
|
+
old[0]?.targetId &&
|
|
337
|
+
!old.some(link => link.ambiguous) &&
|
|
338
|
+
new Set(old.map(link => link.targetId)).size === 1) {
|
|
339
|
+
return { ...old[0], reference: ref };
|
|
340
|
+
}
|
|
341
|
+
const changedOccurrence = old.length === 0 &&
|
|
342
|
+
oldLinks.some(link => link.reference.href === ref.href &&
|
|
343
|
+
link.targetId &&
|
|
344
|
+
(this.record(link.targetId).presence !== 'present' ||
|
|
345
|
+
this.record(link.targetId).path !== target?.path));
|
|
346
|
+
const ambiguous = changedOccurrence ||
|
|
347
|
+
(old.length > 0 &&
|
|
348
|
+
(old.length !== duplicates ||
|
|
349
|
+
new Set(old.map(link => link.targetId)).size > 1));
|
|
350
|
+
if (ambiguous) {
|
|
351
|
+
return {
|
|
352
|
+
reference: ref,
|
|
353
|
+
targetId: null,
|
|
354
|
+
targetPath: target?.path ?? null,
|
|
355
|
+
anchor: target?.anchor ?? '',
|
|
356
|
+
ambiguous: true
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const present = target
|
|
360
|
+
? records.find(note => note.presence === 'present' && note.path === target.path)
|
|
361
|
+
: undefined;
|
|
362
|
+
const historical = target && !present
|
|
363
|
+
? [
|
|
364
|
+
...new Set([
|
|
365
|
+
...this.catalog.historical(target.path),
|
|
366
|
+
...records
|
|
367
|
+
.filter(note => note.path === target.path && note.presence !== 'present')
|
|
368
|
+
.map(note => note.id)
|
|
369
|
+
])
|
|
370
|
+
]
|
|
371
|
+
: [];
|
|
372
|
+
return {
|
|
373
|
+
reference: ref,
|
|
374
|
+
targetId: present?.id ?? (historical.length === 1 ? historical[0] : null),
|
|
375
|
+
targetPath: target?.path ?? null,
|
|
376
|
+
anchor: target?.anchor ?? '',
|
|
377
|
+
ambiguous: historical.length > 1
|
|
378
|
+
};
|
|
379
|
+
})
|
|
380
|
+
.filter(link => link.targetPath?.toLowerCase().endsWith('.md') ||
|
|
381
|
+
link.reference.format === 'wiki');
|
|
382
|
+
this.catalog.setLinks(id, links);
|
|
383
|
+
}
|
|
384
|
+
revision(entries) {
|
|
385
|
+
return sha256Hex(JSON.stringify([
|
|
386
|
+
entries,
|
|
387
|
+
this.catalog
|
|
388
|
+
.all()
|
|
389
|
+
.map(note => [
|
|
390
|
+
note.id,
|
|
391
|
+
note.path,
|
|
392
|
+
note.version,
|
|
393
|
+
note.presence,
|
|
394
|
+
note.deletedAt
|
|
395
|
+
])
|
|
396
|
+
]));
|
|
397
|
+
}
|
|
398
|
+
index() {
|
|
399
|
+
return this.run(async () => {
|
|
400
|
+
const entries = await this.synchronize();
|
|
401
|
+
const records = this.catalog.all();
|
|
402
|
+
const recordsByPath = new Map(records
|
|
403
|
+
.filter(note => note.presence === 'present' && !note.redirectId)
|
|
404
|
+
.map(note => [note.path, note]));
|
|
405
|
+
return {
|
|
406
|
+
storage: this.files.root,
|
|
407
|
+
storageId: this.catalog.storageId,
|
|
408
|
+
revision: this.revision(entries),
|
|
409
|
+
issues: this.catalog
|
|
410
|
+
.issues()
|
|
411
|
+
.filter(issue => !issue.noteId || !this.record(issue.noteId).deletedAt),
|
|
412
|
+
entries: entries.map(entry => {
|
|
413
|
+
const record = recordsByPath.get(entry.path);
|
|
414
|
+
const metadata = entry.folder
|
|
415
|
+
? { tags: [], error: null }
|
|
416
|
+
: (this.cache.get(entry.path)?.metadata ?? {
|
|
417
|
+
tags: [],
|
|
418
|
+
error: null
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
path: entry.path,
|
|
422
|
+
folder: entry.folder,
|
|
423
|
+
...metadata,
|
|
424
|
+
...(record ? { id: record.id, presence: record.presence } : {})
|
|
425
|
+
};
|
|
426
|
+
})
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
descriptor(id, storageId) {
|
|
431
|
+
return this.run(async () => {
|
|
432
|
+
this.assertStorage(storageId);
|
|
433
|
+
await this.synchronize();
|
|
434
|
+
const registered = this.record(id);
|
|
435
|
+
const note = registered.deletedAt
|
|
436
|
+
? { ...registered, presence: 'deleted' }
|
|
437
|
+
: registered.presence === 'present' &&
|
|
438
|
+
!(await this.files.exists(registered.path))
|
|
439
|
+
? { ...registered, presence: 'missing' }
|
|
440
|
+
: registered;
|
|
441
|
+
const backlinks = this.catalog
|
|
442
|
+
.all()
|
|
443
|
+
.filter(source => source.presence === 'present' &&
|
|
444
|
+
!source.deletedAt &&
|
|
445
|
+
this.catalog
|
|
446
|
+
.links(source.id)
|
|
447
|
+
.some(link => !!link.targetId && this.record(link.targetId).id === note.id));
|
|
448
|
+
return { note, backlinks };
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
resolvePath(path) {
|
|
452
|
+
return this.run(async () => {
|
|
453
|
+
await this.synchronize();
|
|
454
|
+
const records = this.catalog.all().filter(note => !note.redirectId);
|
|
455
|
+
const candidates = new Set([
|
|
456
|
+
...records.filter(note => note.path === path).map(note => note.id),
|
|
457
|
+
...this.catalog.historical(path)
|
|
458
|
+
]);
|
|
459
|
+
if (!candidates.size) {
|
|
460
|
+
throw new NotFoundError('The note path was not found.');
|
|
461
|
+
}
|
|
462
|
+
if (candidates.size !== 1) {
|
|
463
|
+
throw new BadRequestError('This path belongs to more than one note over time. Open a stable UUID link instead.');
|
|
464
|
+
}
|
|
465
|
+
const record = this.record([...candidates][0]);
|
|
466
|
+
return record.deletedAt
|
|
467
|
+
? { ...record, presence: 'deleted' }
|
|
468
|
+
: record;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
assertStorage(id) {
|
|
472
|
+
if (id !== this.catalog.storageId) {
|
|
473
|
+
throw new BadRequestError('This link belongs to a different notes storage.');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
readId(id, storageId) {
|
|
477
|
+
return this.run(async () => {
|
|
478
|
+
this.assertStorage(storageId);
|
|
479
|
+
await this.synchronize();
|
|
480
|
+
const record = this.record(id);
|
|
481
|
+
if (record.presence !== 'present' || record.deletedAt) {
|
|
482
|
+
throw new NotFoundError('The note file is missing or deleted.');
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
...(await this.files.read(record.path)),
|
|
486
|
+
id: record.id,
|
|
487
|
+
storageId
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
readPath(path) {
|
|
492
|
+
return this.run(async () => {
|
|
493
|
+
await this.synchronize();
|
|
494
|
+
const record = this.catalog
|
|
495
|
+
.all()
|
|
496
|
+
.find(note => note.path === path &&
|
|
497
|
+
note.presence === 'present' &&
|
|
498
|
+
!note.redirectId);
|
|
499
|
+
if (!record || record.deletedAt) {
|
|
500
|
+
throw new NotFoundError('Note not found.');
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
...(await this.files.read(path)),
|
|
504
|
+
id: record.id,
|
|
505
|
+
storageId: this.catalog.storageId
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
save(path, text, version, id) {
|
|
510
|
+
return this.run(async () => {
|
|
511
|
+
const metadata = noteMetadata(text);
|
|
512
|
+
if (metadata.error)
|
|
513
|
+
throw new BadRequestError(metadata.error);
|
|
514
|
+
await this.files.path(path);
|
|
515
|
+
await this.writer.acquire(true);
|
|
516
|
+
await this.synchronize();
|
|
517
|
+
if (id) {
|
|
518
|
+
const identified = this.record(id);
|
|
519
|
+
if (identified.deletedAt) {
|
|
520
|
+
throw new NotFoundError('This note was deleted.');
|
|
521
|
+
}
|
|
522
|
+
path = identified.path;
|
|
523
|
+
}
|
|
524
|
+
let record = this.catalog
|
|
525
|
+
.all()
|
|
526
|
+
.find(note => note.path === path &&
|
|
527
|
+
note.presence === 'present' &&
|
|
528
|
+
!note.redirectId);
|
|
529
|
+
if (this.catalog.all().some(note => note.path === path && note.deletedAt)) {
|
|
530
|
+
throw new NoteExistsError();
|
|
531
|
+
}
|
|
532
|
+
if (version === undefined) {
|
|
533
|
+
portablePath(path);
|
|
534
|
+
if (record)
|
|
535
|
+
throw new NoteExistsError();
|
|
536
|
+
record = newRecord(randomUUID(), path, '');
|
|
537
|
+
text = withNoteIdentity(text, record.id);
|
|
538
|
+
}
|
|
539
|
+
if (!record)
|
|
540
|
+
throw new NotFoundError('Note identity not found.');
|
|
541
|
+
await this.writer.assertOwner();
|
|
542
|
+
const disk = await this.files.save(path, text, version);
|
|
543
|
+
this.catalog.put({
|
|
544
|
+
...record,
|
|
545
|
+
version: disk.version,
|
|
546
|
+
updatedAt: new Date().toISOString()
|
|
547
|
+
});
|
|
548
|
+
this.indexLinks(record.id, disk.text);
|
|
549
|
+
return { ...disk, id: record.id, storageId: this.catalog.storageId };
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
saveId(id, storageId, text, version) {
|
|
553
|
+
this.assertStorage(storageId);
|
|
554
|
+
return this.save(this.record(id).path, text, version, id);
|
|
555
|
+
}
|
|
556
|
+
folder(path) {
|
|
557
|
+
return this.run(async () => {
|
|
558
|
+
portablePath(path);
|
|
559
|
+
await this.files.path(path, false);
|
|
560
|
+
await this.writer.acquire(true);
|
|
561
|
+
await this.writer.assertOwner();
|
|
562
|
+
await this.files.folder(path);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
rename(path, name, folder, target, id, expectedRevision) {
|
|
566
|
+
return this.run(async () => {
|
|
567
|
+
const entries = await this.synchronize();
|
|
568
|
+
if (expectedRevision && expectedRevision !== this.revision(entries)) {
|
|
569
|
+
throw new NoteConflictError();
|
|
570
|
+
}
|
|
571
|
+
if (id && this.record(id).path !== path)
|
|
572
|
+
throw new NoteConflictError();
|
|
573
|
+
if (!folder &&
|
|
574
|
+
this.catalog.all().some(note => note.path === path && note.deletedAt)) {
|
|
575
|
+
throw new NotFoundError('This note was deleted.');
|
|
576
|
+
}
|
|
577
|
+
if (!target &&
|
|
578
|
+
(name !== name.trim() ||
|
|
579
|
+
name.includes('/') ||
|
|
580
|
+
name.includes('\\') ||
|
|
581
|
+
name.startsWith('.'))) {
|
|
582
|
+
throw new BadRequestError('Invalid entry name.');
|
|
583
|
+
}
|
|
584
|
+
const sourcePath = await this.files.path(path, !folder);
|
|
585
|
+
if (path.split('/')[0] === 'assets') {
|
|
586
|
+
throw new BadRequestError('Cannot rename attachments.');
|
|
587
|
+
}
|
|
588
|
+
const sourceStat = await lstat(sourcePath);
|
|
589
|
+
if (folder ? !sourceStat.isDirectory() : !sourceStat.isFile()) {
|
|
590
|
+
throw new BadRequestError('Entry type changed.');
|
|
591
|
+
}
|
|
592
|
+
const destination = target ?? noteChildPath(noteParentPath(path), name);
|
|
593
|
+
portablePath(destination);
|
|
594
|
+
await this.files.path(destination, !folder);
|
|
595
|
+
if (destination === path) {
|
|
596
|
+
return { operationId: randomUUID(), complete: true };
|
|
597
|
+
}
|
|
598
|
+
if (this.catalog
|
|
599
|
+
.all()
|
|
600
|
+
.some(note => note.deletedAt && note.path === destination)) {
|
|
601
|
+
throw new NoteExistsError();
|
|
602
|
+
}
|
|
603
|
+
if (await this.files.exists(destination)) {
|
|
604
|
+
const caseOnly = process.platform === 'win32' &&
|
|
605
|
+
path.toLowerCase() === destination.toLowerCase() &&
|
|
606
|
+
(await this.files.sameEntry(path, destination));
|
|
607
|
+
if (!caseOnly)
|
|
608
|
+
throw new NoteExistsError();
|
|
609
|
+
}
|
|
610
|
+
if (folder && destination.startsWith(path + '/')) {
|
|
611
|
+
throw new BadRequestError('Cannot move a directory into itself.');
|
|
612
|
+
}
|
|
613
|
+
const moves = this.catalog
|
|
614
|
+
.all()
|
|
615
|
+
.filter(note => note.presence === 'present' &&
|
|
616
|
+
(note.path === path || (folder && note.path.startsWith(path + '/'))))
|
|
617
|
+
.map(note => ({
|
|
618
|
+
id: note.id,
|
|
619
|
+
from: note.path,
|
|
620
|
+
to: destination + note.path.slice(path.length)
|
|
621
|
+
}));
|
|
622
|
+
const operation = await this.operations.prepare(moves, path, destination, folder, false);
|
|
623
|
+
let complete;
|
|
624
|
+
try {
|
|
625
|
+
complete = await this.operations.execute(operation);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
if (!operation.moved &&
|
|
629
|
+
!(await this.files.exists(destination)) &&
|
|
630
|
+
(!operation.temporary ||
|
|
631
|
+
!(await this.files.exists(operation.temporary)))) {
|
|
632
|
+
this.catalog.finishOperation(operation.id);
|
|
633
|
+
}
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
return { operationId: operation.id, complete };
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
deleteId(id, storageId) {
|
|
640
|
+
return this.run(async () => {
|
|
641
|
+
this.assertStorage(storageId);
|
|
642
|
+
await this.synchronize();
|
|
643
|
+
const note = this.record(id);
|
|
644
|
+
if (note.deletedAt)
|
|
645
|
+
return;
|
|
646
|
+
await this.writer.acquire(true);
|
|
647
|
+
await this.writer.assertOwner();
|
|
648
|
+
if (this.catalog
|
|
649
|
+
.operations()
|
|
650
|
+
.some(operation => operation.moves.some(move => move.id === note.id) ||
|
|
651
|
+
operation.writes.some(write => write.id === note.id && !write.done))) {
|
|
652
|
+
throw new NoteConflictError();
|
|
653
|
+
}
|
|
654
|
+
// The note leaves its name, so a new note can use it. The catalog keeps the identity.
|
|
655
|
+
// Only a present file exists on disk and needs the move.
|
|
656
|
+
const path = deletedNotePath(note.path, randomUUID());
|
|
657
|
+
if (note.presence === 'present' && !note.redirectId) {
|
|
658
|
+
await this.files.moveEntry(note.path, path, false);
|
|
659
|
+
}
|
|
660
|
+
this.catalog.put({
|
|
661
|
+
...note,
|
|
662
|
+
path,
|
|
663
|
+
deletedAt: new Date().toISOString()
|
|
664
|
+
});
|
|
665
|
+
this.catalog.dismiss('missing:' + note.id, true);
|
|
666
|
+
this.catalog.dismiss('metadata:' + note.id);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
retry() {
|
|
670
|
+
return this.run(() => this.operations.retry());
|
|
671
|
+
}
|
|
672
|
+
resolveIdentity(issueId, restore, candidateId) {
|
|
673
|
+
return this.run(async () => {
|
|
674
|
+
await this.synchronize();
|
|
675
|
+
const issue = this.catalog.issues().find(item => item.id === issueId);
|
|
676
|
+
if (!issue || issue.kind !== 'identity' || !issue.noteId) {
|
|
677
|
+
throw new BadRequestError('Identity candidate is no longer available.');
|
|
678
|
+
}
|
|
679
|
+
const old = this.record(issue.noteId);
|
|
680
|
+
if (old.presence === 'present')
|
|
681
|
+
throw new NoteConflictError();
|
|
682
|
+
const selectedCandidate = candidateId ?? issue.candidateId;
|
|
683
|
+
if (!selectedCandidate) {
|
|
684
|
+
if (restore)
|
|
685
|
+
throw new BadRequestError('Select the moved file first.');
|
|
686
|
+
this.catalog.put({ ...old, presence: 'deleted' });
|
|
687
|
+
this.catalog.dismiss(issueId, true);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const candidate = this.record(selectedCandidate);
|
|
691
|
+
if (candidate.presence !== 'present')
|
|
692
|
+
throw new NoteConflictError();
|
|
693
|
+
if (restore) {
|
|
694
|
+
await this.files.read(candidate.path);
|
|
695
|
+
const changes = [{ id: old.id, from: old.path, to: candidate.path }];
|
|
696
|
+
const previousLinks = this.catalog.links(old.id);
|
|
697
|
+
const candidateLinks = this.catalog.links(candidate.id).map(link => {
|
|
698
|
+
const previous = previousLinks.filter(item => sameReference(item.reference, link.reference));
|
|
699
|
+
return previous.length === 1
|
|
700
|
+
? { ...previous[0], reference: link.reference }
|
|
701
|
+
: link;
|
|
702
|
+
});
|
|
703
|
+
const preview = this.catalog
|
|
704
|
+
.all()
|
|
705
|
+
.filter(note => note.id !== old.id && note.id !== candidate.id);
|
|
706
|
+
preview.push({
|
|
707
|
+
...old,
|
|
708
|
+
path: candidate.path,
|
|
709
|
+
version: candidate.version,
|
|
710
|
+
presence: 'present'
|
|
711
|
+
});
|
|
712
|
+
const operation = await this.operations.prepare(changes, '', '', false, true, preview, {
|
|
713
|
+
previousId: old.id,
|
|
714
|
+
candidateId: candidate.id,
|
|
715
|
+
version: candidate.version,
|
|
716
|
+
links: candidateLinks
|
|
717
|
+
});
|
|
718
|
+
await this.operations.execute(operation);
|
|
719
|
+
}
|
|
720
|
+
this.catalog.dismiss(issueId, true);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
resolve(sourceId, storageId, text, href, occurrence) {
|
|
724
|
+
return this.run(async () => {
|
|
725
|
+
this.assertStorage(storageId);
|
|
726
|
+
await this.synchronize();
|
|
727
|
+
const source = this.record(sourceId);
|
|
728
|
+
if (source.deletedAt)
|
|
729
|
+
throw new NotFoundError('This note was deleted.');
|
|
730
|
+
const paths = this.catalog
|
|
731
|
+
.all()
|
|
732
|
+
.filter(note => note.presence === 'present' && !note.redirectId && !note.deletedAt)
|
|
733
|
+
.map(note => note.path);
|
|
734
|
+
const refs = noteReferences(text)
|
|
735
|
+
.flatMap(ref => (ref.displayStarts ?? [ref.start]).map(displayPosition => ({
|
|
736
|
+
...ref,
|
|
737
|
+
displayPosition
|
|
738
|
+
})))
|
|
739
|
+
.sort((a, b) => a.displayPosition - b.displayPosition)
|
|
740
|
+
.filter(ref => !ref.image &&
|
|
741
|
+
(ref.href === href ||
|
|
742
|
+
(() => {
|
|
743
|
+
const target = referenceTarget(ref, source.path, paths);
|
|
744
|
+
const clicked = referenceTarget({ ...ref, format: 'markdown', href }, source.path, paths);
|
|
745
|
+
return (!!target &&
|
|
746
|
+
target.path === clicked?.path &&
|
|
747
|
+
target.anchor === clicked.anchor);
|
|
748
|
+
})()));
|
|
749
|
+
const links = this.catalog.links(source.id);
|
|
750
|
+
const candidates = occurrence === undefined ? refs : refs.slice(occurrence, occurrence + 1);
|
|
751
|
+
const targets = new Set();
|
|
752
|
+
let anchor = '';
|
|
753
|
+
for (const ref of candidates) {
|
|
754
|
+
const known = links.filter(link => sameReference(link.reference, ref));
|
|
755
|
+
if (known.some(link => link.ambiguous) ||
|
|
756
|
+
new Set(known.map(link => link.targetId)).size > 1) {
|
|
757
|
+
throw new BadRequestError('This link is ambiguous. Choose its target explicitly.');
|
|
758
|
+
}
|
|
759
|
+
const target = referenceTarget(ref, source.path, paths);
|
|
760
|
+
anchor = target?.anchor ?? '';
|
|
761
|
+
const old = known[0];
|
|
762
|
+
anchor = old?.anchor ?? anchor;
|
|
763
|
+
if (old?.targetId) {
|
|
764
|
+
targets.add(this.record(old.targetId).id);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
// A changed context with the same old address must not hijack a missing target.
|
|
768
|
+
if (links.some(link => link.reference.href === ref.href &&
|
|
769
|
+
link.targetId &&
|
|
770
|
+
(this.record(link.targetId).presence !== 'present' ||
|
|
771
|
+
this.record(link.targetId).path !== target?.path))) {
|
|
772
|
+
throw new BadRequestError('The previous target is missing and this occurrence cannot be identified safely.');
|
|
773
|
+
}
|
|
774
|
+
const present = this.catalog
|
|
775
|
+
.all()
|
|
776
|
+
.find(note => note.presence === 'present' &&
|
|
777
|
+
note.path === target?.path &&
|
|
778
|
+
!note.redirectId);
|
|
779
|
+
if (present)
|
|
780
|
+
targets.add(present.id);
|
|
781
|
+
}
|
|
782
|
+
if (!refs.length && href.startsWith('#')) {
|
|
783
|
+
targets.add(source.id);
|
|
784
|
+
anchor = href.slice(1);
|
|
785
|
+
}
|
|
786
|
+
if (targets.size !== 1) {
|
|
787
|
+
throw new BadRequestError('This link is missing or ambiguous. Edit its destination to choose a note.');
|
|
788
|
+
}
|
|
789
|
+
return { id: [...targets][0], anchor: safeDecodeURIComponent(anchor) };
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
graph() {
|
|
793
|
+
return this.run(async () => {
|
|
794
|
+
await this.synchronize();
|
|
795
|
+
const nodes = this.catalog
|
|
796
|
+
.all()
|
|
797
|
+
.filter(note => !note.redirectId && !note.deletedAt);
|
|
798
|
+
const edges = nodes
|
|
799
|
+
.filter(note => note.presence === 'present')
|
|
800
|
+
.flatMap(note => this.catalog.links(note.id).flatMap(link => link.targetId && !this.record(link.targetId).deletedAt
|
|
801
|
+
? [
|
|
802
|
+
{
|
|
803
|
+
sourceId: note.id,
|
|
804
|
+
targetId: this.record(link.targetId).id
|
|
805
|
+
}
|
|
806
|
+
]
|
|
807
|
+
: []));
|
|
808
|
+
return { storageId: this.catalog.storageId, nodes, edges };
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|