@btst/stack 2.6.2 → 2.8.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 +1 -0
- package/dist/api/index.d.cts +2 -2
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.d.ts +2 -2
- package/dist/client/index.d.cts +2 -2
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/components/auto-form/index.d.cts +2 -2
- package/dist/components/auto-form/index.d.mts +2 -2
- package/dist/components/auto-form/index.d.ts +2 -2
- package/dist/components/form-builder/index.d.cts +1 -1
- package/dist/components/form-builder/index.d.mts +1 -1
- package/dist/components/form-builder/index.d.ts +1 -1
- package/dist/components/stepped-auto-form/index.d.cts +1 -1
- package/dist/components/stepped-auto-form/index.d.mts +1 -1
- package/dist/components/stepped-auto-form/index.d.ts +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.cjs +13 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.mjs +11 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.cjs +17 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.mjs +15 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -7
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -7
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.cjs +48 -52
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.mjs +49 -53
- package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.cjs +34 -37
- package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.mjs +35 -38
- package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.cjs +4 -21
- package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.mjs +4 -21
- package/dist/packages/stack/src/plugins/comments/api/getters.cjs +284 -0
- package/dist/packages/stack/src/plugins/comments/api/getters.mjs +280 -0
- package/dist/packages/stack/src/plugins/comments/api/mutations.cjs +118 -0
- package/dist/packages/stack/src/plugins/comments/api/mutations.mjs +112 -0
- package/dist/packages/stack/src/plugins/comments/api/plugin.cjs +335 -0
- package/dist/packages/stack/src/plugins/comments/api/plugin.mjs +333 -0
- package/dist/packages/stack/src/plugins/comments/api/query-key-defs.cjs +60 -0
- package/dist/packages/stack/src/plugins/comments/api/query-key-defs.mjs +55 -0
- package/dist/packages/stack/src/plugins/comments/api/serializers.cjs +23 -0
- package/dist/packages/stack/src/plugins/comments/api/serializers.mjs +21 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-count.cjs +46 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-count.mjs +44 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-form.cjs +86 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-form.mjs +84 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.cjs +540 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.mjs +538 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.cjs +64 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.cjs +426 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.mjs +424 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.mjs +62 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.cjs +66 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.cjs +256 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.mjs +254 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.mjs +64 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.cjs +86 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.cjs +191 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.mjs +189 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.mjs +84 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.cjs +27 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.mjs +25 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.cjs +37 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.mjs +35 -0
- package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.cjs +476 -0
- package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.mjs +464 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.cjs +67 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.mjs +65 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.cjs +27 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.mjs +25 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.cjs +30 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.mjs +28 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/index.cjs +13 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/index.mjs +11 -0
- package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +116 -0
- package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +114 -0
- package/dist/packages/stack/src/plugins/comments/client/utils.cjs +41 -0
- package/dist/packages/stack/src/plugins/comments/client/utils.mjs +37 -0
- package/dist/packages/stack/src/plugins/comments/db.cjs +75 -0
- package/dist/packages/stack/src/plugins/comments/db.mjs +73 -0
- package/dist/packages/stack/src/plugins/comments/schemas.cjs +45 -0
- package/dist/packages/stack/src/plugins/comments/schemas.mjs +38 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +5 -4
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +5 -4
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +0 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +0 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.cjs +39 -22
- package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.mjs +40 -23
- package/dist/packages/ui/src/components/avatar.mjs +1 -1
- package/dist/packages/ui/src/components/pagination-controls.cjs +64 -0
- package/dist/packages/ui/src/components/pagination-controls.mjs +62 -0
- package/dist/packages/ui/src/components/when-visible.cjs +39 -0
- package/dist/packages/ui/src/components/when-visible.mjs +37 -0
- package/dist/plugins/ai-chat/api/index.d.cts +4 -6
- package/dist/plugins/ai-chat/api/index.d.mts +4 -6
- package/dist/plugins/ai-chat/api/index.d.ts +4 -6
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -3
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -3
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -3
- package/dist/plugins/ai-chat/query-keys.d.cts +1 -3
- package/dist/plugins/ai-chat/query-keys.d.mts +1 -3
- package/dist/plugins/ai-chat/query-keys.d.ts +1 -3
- package/dist/plugins/api/index.d.cts +3 -3
- package/dist/plugins/api/index.d.mts +3 -3
- package/dist/plugins/api/index.d.ts +3 -3
- package/dist/plugins/blog/api/index.d.cts +3 -3
- package/dist/plugins/blog/api/index.d.mts +3 -3
- package/dist/plugins/blog/api/index.d.ts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +25 -3
- package/dist/plugins/blog/client/index.d.mts +25 -3
- package/dist/plugins/blog/client/index.d.ts +25 -3
- package/dist/plugins/blog/query-keys.d.cts +3 -3
- package/dist/plugins/blog/query-keys.d.mts +3 -3
- package/dist/plugins/blog/query-keys.d.ts +3 -3
- package/dist/plugins/client/index.d.cts +2 -2
- package/dist/plugins/client/index.d.mts +2 -2
- package/dist/plugins/client/index.d.ts +2 -2
- package/dist/plugins/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/cms/client/index.d.cts +1 -1
- package/dist/plugins/cms/client/index.d.mts +1 -1
- package/dist/plugins/cms/client/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- package/dist/plugins/comments/api/index.cjs +21 -0
- package/dist/plugins/comments/api/index.d.cts +126 -0
- package/dist/plugins/comments/api/index.d.mts +126 -0
- package/dist/plugins/comments/api/index.d.ts +126 -0
- package/dist/plugins/comments/api/index.mjs +5 -0
- package/dist/plugins/comments/client/components/index.cjs +15 -0
- package/dist/plugins/comments/client/components/index.d.cts +125 -0
- package/dist/plugins/comments/client/components/index.d.mts +125 -0
- package/dist/plugins/comments/client/components/index.d.ts +125 -0
- package/dist/plugins/comments/client/components/index.mjs +5 -0
- package/dist/plugins/comments/client/hooks/index.cjs +17 -0
- package/dist/plugins/comments/client/hooks/index.d.cts +200 -0
- package/dist/plugins/comments/client/hooks/index.d.mts +200 -0
- package/dist/plugins/comments/client/hooks/index.d.ts +200 -0
- package/dist/plugins/comments/client/hooks/index.mjs +1 -0
- package/dist/plugins/comments/client/index.cjs +9 -0
- package/dist/plugins/comments/client/index.d.cts +262 -0
- package/dist/plugins/comments/client/index.d.mts +262 -0
- package/dist/plugins/comments/client/index.d.ts +262 -0
- package/dist/plugins/comments/client/index.mjs +2 -0
- package/dist/plugins/comments/client.css +2 -0
- package/dist/plugins/comments/query-keys.cjs +113 -0
- package/dist/plugins/comments/query-keys.d.cts +71 -0
- package/dist/plugins/comments/query-keys.d.mts +71 -0
- package/dist/plugins/comments/query-keys.d.ts +71 -0
- package/dist/plugins/comments/query-keys.mjs +111 -0
- package/dist/plugins/comments/style.css +15 -0
- package/dist/plugins/form-builder/api/index.d.cts +2 -2
- package/dist/plugins/form-builder/api/index.d.mts +2 -2
- package/dist/plugins/form-builder/api/index.d.ts +2 -2
- package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.d.cts +1 -1
- package/dist/plugins/form-builder/query-keys.d.mts +1 -1
- package/dist/plugins/form-builder/query-keys.d.ts +1 -1
- package/dist/plugins/kanban/api/index.d.cts +2 -2
- package/dist/plugins/kanban/api/index.d.mts +2 -2
- package/dist/plugins/kanban/api/index.d.ts +2 -2
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +2 -2
- package/dist/plugins/kanban/query-keys.d.mts +2 -2
- package/dist/plugins/kanban/query-keys.d.ts +2 -2
- package/dist/plugins/open-api/api/index.d.cts +3 -3
- package/dist/plugins/open-api/api/index.d.mts +3 -3
- package/dist/plugins/open-api/api/index.d.ts +3 -3
- package/dist/plugins/route-docs/client/index.d.cts +1 -1
- package/dist/plugins/route-docs/client/index.d.mts +1 -1
- package/dist/plugins/route-docs/client/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/components/index.d.cts +2 -2
- package/dist/plugins/ui-builder/client/components/index.d.mts +2 -2
- package/dist/plugins/ui-builder/client/components/index.d.ts +2 -2
- package/dist/plugins/ui-builder/client/hooks/index.d.cts +3 -3
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +3 -3
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +3 -3
- package/dist/plugins/ui-builder/client/index.d.cts +3 -3
- package/dist/plugins/ui-builder/client/index.d.mts +3 -3
- package/dist/plugins/ui-builder/client/index.d.ts +3 -3
- package/dist/plugins/ui-builder/index.d.cts +3 -3
- package/dist/plugins/ui-builder/index.d.mts +3 -3
- package/dist/plugins/ui-builder/index.d.ts +3 -3
- package/dist/shared/{stack.B1srlBud.d.mts → stack.BFoBvGML.d.mts} +1 -1
- package/dist/shared/{stack.DmpPDPxA.d.cts → stack.BOCvd9HK.d.cts} +1 -1
- package/dist/shared/{stack.n1_i1p2B.d.cts → stack.BOokfhZD.d.cts} +170 -110
- package/dist/shared/{stack.DXnclTG7.d.ts → stack.BSqJrCTM.d.cts} +120 -59
- package/dist/shared/{stack.B58oHdqm.d.mts → stack.BX7MHi0J.d.mts} +90 -45
- package/dist/shared/{stack.cfCkioTe.d.mts → stack.BXxrFL9R.d.ts} +120 -59
- package/dist/shared/{stack.CSx98K5H.d.cts → stack.BYN8wCV6.d.cts} +87 -58
- package/dist/shared/{stack.FVWf2JhZ.d.mts → stack.BgQrdSlo.d.mts} +60 -45
- package/dist/shared/{stack.BK9Z2dcL.d.ts → stack.BmMB0LNC.d.ts} +1 -1
- package/dist/shared/{stack.j75TpKh2.d.ts → stack.BvCR4-9H.d.ts} +170 -110
- package/dist/shared/{stack.FeaWkglm.d.ts → stack.BxFl46lB.d.cts} +24 -1
- package/dist/shared/stack.C-b3Sn8j.d.cts +142 -0
- package/dist/shared/stack.C-b3Sn8j.d.mts +142 -0
- package/dist/shared/stack.C-b3Sn8j.d.ts +142 -0
- package/dist/shared/{stack.CFECM0ew.d.cts → stack.C1nXGBr6.d.cts} +1 -1
- package/dist/shared/{stack.C9Mg2Q46.d.cts → stack.C9zoS1TN.d.cts} +90 -45
- package/dist/shared/stack.CJE9sAjV.d.ts +335 -0
- package/dist/shared/{stack.fdi94T4S.d.mts → stack.CPsYC2-Z.d.cts} +7 -7
- package/dist/shared/{stack.fdi94T4S.d.ts → stack.CPsYC2-Z.d.mts} +7 -7
- package/dist/shared/{stack.fdi94T4S.d.cts → stack.CPsYC2-Z.d.ts} +7 -7
- package/dist/shared/{stack.7n9Y_u7N.d.cts → stack.CQnwAN7x.d.cts} +6 -6
- package/dist/shared/{stack.7n9Y_u7N.d.mts → stack.CQnwAN7x.d.mts} +6 -6
- package/dist/shared/{stack.7n9Y_u7N.d.ts → stack.CQnwAN7x.d.ts} +6 -6
- package/dist/shared/{stack.CxaFNQCV.d.mts → stack.CWxAl9K3.d.mts} +170 -110
- package/dist/shared/{stack.D-b5zbPm.d.cts → stack.Cbsrl06u.d.cts} +60 -45
- package/dist/shared/stack.CmHRdhl8.d.cts +335 -0
- package/dist/shared/{stack.BgTmujxW.d.mts → stack.D88yU4FT.d.mts} +87 -58
- package/dist/shared/{stack.DVtk5CNw.d.mts → stack.DLPa6Gzm.d.mts} +1 -1
- package/dist/shared/{stack.BAT540yW.d.ts → stack.DOZ1EXjM.d.mts} +9 -15
- package/dist/shared/{stack.FeaWkglm.d.mts → stack.DRpeDS6X.d.ts} +24 -1
- package/dist/shared/{stack.B8vT-Yt4.d.mts → stack.DX-tQ93o.d.cts} +9 -15
- package/dist/shared/stack.Dcz6636A.d.mts +335 -0
- package/dist/shared/{stack.ASwEoINr.d.ts → stack.DxJ-tHLt.d.ts} +1 -1
- package/dist/shared/{stack.DaZM10cp.d.cts → stack.DzOhpIYM.d.mts} +120 -59
- package/dist/shared/{stack.CTDVxbrA.d.ts → stack.Fl2Kl_bt.d.ts} +60 -45
- package/dist/shared/{stack.FeaWkglm.d.cts → stack.Jb0kQDJC.d.mts} +24 -1
- package/dist/shared/stack.Ldfkr5b2.d.cts +112 -0
- package/dist/shared/stack.Ldfkr5b2.d.mts +112 -0
- package/dist/shared/stack.Ldfkr5b2.d.ts +112 -0
- package/dist/shared/{stack.CLQuVdwK.d.ts → stack.RuQ9JCLo.d.ts} +87 -58
- package/dist/shared/{stack.BwA7trxA.d.cts → stack.VF6FhyZw.d.ts} +9 -15
- package/dist/shared/{stack.sO33ZDhK.d.ts → stack.fQjVhw5a.d.ts} +90 -45
- package/package.json +70 -5
- package/src/__tests__/plugins.test.tsx +5 -1
- package/src/__tests__/stack-api.test.ts +1 -1
- package/src/plugins/ai-chat/__tests__/getters.test.ts +1 -1
- package/src/plugins/ai-chat/api/getters.ts +1 -1
- package/src/plugins/ai-chat/api/plugin.ts +1 -1
- package/src/plugins/api/index.ts +5 -1
- package/src/plugins/blog/__tests__/getters.test.ts +1 -1
- package/src/plugins/blog/api/getters.ts +1 -1
- package/src/plugins/blog/api/plugin.ts +1 -1
- package/src/plugins/blog/client/components/loading/post-navigation-skeleton.tsx +10 -0
- package/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.tsx +18 -0
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +23 -8
- package/src/plugins/blog/client/components/shared/post-navigation.tsx +0 -5
- package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +1 -5
- package/src/plugins/blog/client/hooks/blog-hooks.tsx +8 -33
- package/src/plugins/blog/client/overrides.ts +26 -1
- package/src/plugins/cms/__tests__/getters.test.ts +1 -1
- package/src/plugins/cms/api/getters.ts +1 -1
- package/src/plugins/cms/api/mutations.ts +1 -1
- package/src/plugins/cms/api/plugin.ts +1 -1
- package/src/plugins/cms/client/components/shared/pagination.tsx +14 -42
- package/src/plugins/comments/api/getters.ts +444 -0
- package/src/plugins/comments/api/index.ts +21 -0
- package/src/plugins/comments/api/mutations.ts +206 -0
- package/src/plugins/comments/api/plugin.ts +628 -0
- package/src/plugins/comments/api/query-key-defs.ts +143 -0
- package/src/plugins/comments/api/serializers.ts +37 -0
- package/src/plugins/comments/client/components/comment-count.tsx +66 -0
- package/src/plugins/comments/client/components/comment-form.tsx +112 -0
- package/src/plugins/comments/client/components/comment-thread.tsx +799 -0
- package/src/plugins/comments/client/components/index.tsx +11 -0
- package/src/plugins/comments/client/components/pages/moderation-page.internal.tsx +550 -0
- package/src/plugins/comments/client/components/pages/moderation-page.tsx +70 -0
- package/src/plugins/comments/client/components/pages/my-comments-page.internal.tsx +367 -0
- package/src/plugins/comments/client/components/pages/my-comments-page.tsx +72 -0
- package/src/plugins/comments/client/components/pages/resource-comments-page.internal.tsx +225 -0
- package/src/plugins/comments/client/components/pages/resource-comments-page.tsx +97 -0
- package/src/plugins/comments/client/components/shared/page-wrapper.tsx +32 -0
- package/src/plugins/comments/client/components/shared/pagination.tsx +44 -0
- package/src/plugins/comments/client/hooks/index.tsx +13 -0
- package/src/plugins/comments/client/hooks/use-comments.tsx +717 -0
- package/src/plugins/comments/client/index.ts +14 -0
- package/src/plugins/comments/client/localization/comments-moderation.ts +75 -0
- package/src/plugins/comments/client/localization/comments-my.ts +32 -0
- package/src/plugins/comments/client/localization/comments-thread.ts +32 -0
- package/src/plugins/comments/client/localization/index.ts +11 -0
- package/src/plugins/comments/client/overrides.ts +164 -0
- package/src/plugins/comments/client/plugin.tsx +195 -0
- package/src/plugins/comments/client/utils.ts +67 -0
- package/src/plugins/comments/client.css +2 -0
- package/src/plugins/comments/db.ts +77 -0
- package/src/plugins/comments/query-keys.ts +189 -0
- package/src/plugins/comments/schemas.ts +72 -0
- package/src/plugins/comments/style.css +15 -0
- package/src/plugins/comments/types.ts +73 -0
- package/src/plugins/form-builder/__tests__/getters.test.ts +1 -1
- package/src/plugins/form-builder/api/getters.ts +1 -1
- package/src/plugins/form-builder/api/plugin.ts +1 -1
- package/src/plugins/kanban/__tests__/getters.test.ts +1 -1
- package/src/plugins/kanban/api/getters.ts +1 -1
- package/src/plugins/kanban/api/mutations.ts +1 -1
- package/src/plugins/kanban/api/plugin.ts +6 -5
- package/src/plugins/kanban/client/components/forms/task-form.tsx +0 -1
- package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +46 -27
- package/src/plugins/kanban/client/overrides.ts +27 -1
- package/src/types.ts +5 -1
- package/dist/shared/{stack.BQmuNl5p.d.mts → stack.BWp0hcm9.d.cts} +3 -3
- package/dist/shared/{stack.BQmuNl5p.d.ts → stack.BWp0hcm9.d.mts} +3 -3
- package/dist/shared/{stack.BQmuNl5p.d.cts → stack.BWp0hcm9.d.ts} +3 -3
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import type { DBAdapter as Adapter } from "@btst/db";
|
|
2
|
+
import { defineBackendPlugin, createEndpoint } from "@btst/stack/plugins/api";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { commentsSchema as dbSchema } from "../db";
|
|
5
|
+
import type { Comment } from "../types";
|
|
6
|
+
import {
|
|
7
|
+
CommentListQuerySchema,
|
|
8
|
+
CommentListParamsSchema,
|
|
9
|
+
CommentCountQuerySchema,
|
|
10
|
+
createCommentSchema,
|
|
11
|
+
updateCommentSchema,
|
|
12
|
+
updateCommentStatusSchema,
|
|
13
|
+
} from "../schemas";
|
|
14
|
+
import { listComments, getCommentById, getCommentCount } from "./getters";
|
|
15
|
+
import {
|
|
16
|
+
createComment,
|
|
17
|
+
updateComment,
|
|
18
|
+
updateCommentStatus,
|
|
19
|
+
deleteComment,
|
|
20
|
+
toggleCommentLike,
|
|
21
|
+
} from "./mutations";
|
|
22
|
+
import { runHookWithShim } from "../../utils";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Context passed to comments API hooks
|
|
26
|
+
*/
|
|
27
|
+
export interface CommentsApiContext {
|
|
28
|
+
body?: unknown;
|
|
29
|
+
params?: unknown;
|
|
30
|
+
query?: unknown;
|
|
31
|
+
request?: Request;
|
|
32
|
+
headers?: Headers;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Shared hook and config fields that are always present regardless of allowPosting. */
|
|
37
|
+
interface CommentsBackendOptionsBase {
|
|
38
|
+
/**
|
|
39
|
+
* When true, new comments are automatically approved (status: "approved").
|
|
40
|
+
* Default: false — all comments start as "pending" until a moderator approves.
|
|
41
|
+
*/
|
|
42
|
+
autoApprove?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* When false, the `PATCH /comments/:id` endpoint is not registered and
|
|
46
|
+
* comment bodies cannot be edited.
|
|
47
|
+
* Default: true.
|
|
48
|
+
*/
|
|
49
|
+
allowEditing?: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Server-side user resolution hook. Called once per unique authorId when
|
|
53
|
+
* serving GET /comments. Return null for deleted/unknown users (shown as "[deleted]").
|
|
54
|
+
* Deduplicates lookups — each unique authorId is resolved only once per request.
|
|
55
|
+
*/
|
|
56
|
+
resolveUser?: (
|
|
57
|
+
authorId: string,
|
|
58
|
+
) => Promise<{ name: string; avatarUrl?: string } | null>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Called before the comment list or count is returned. Throw to reject.
|
|
62
|
+
* When this hook is absent, any request with `status` other than "approved"
|
|
63
|
+
* is automatically rejected with 403 on both `GET /comments` and
|
|
64
|
+
* `GET /comments/count` — preventing anonymous callers from reading or
|
|
65
|
+
* probing the pending/spam moderation queues. Configure this hook to
|
|
66
|
+
* authorize admin callers (e.g. check session role).
|
|
67
|
+
*/
|
|
68
|
+
onBeforeList?: (
|
|
69
|
+
query: z.infer<typeof CommentListQuerySchema>,
|
|
70
|
+
context: CommentsApiContext,
|
|
71
|
+
) => Promise<void> | void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Called after a comment is successfully created.
|
|
75
|
+
*/
|
|
76
|
+
onAfterPost?: (
|
|
77
|
+
comment: Comment,
|
|
78
|
+
context: CommentsApiContext,
|
|
79
|
+
) => Promise<void> | void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Called before a comment body is edited. Throw an error to reject the edit.
|
|
83
|
+
* Use this to enforce that only the comment owner can edit (compare authorId to session).
|
|
84
|
+
*/
|
|
85
|
+
onBeforeEdit?: (
|
|
86
|
+
commentId: string,
|
|
87
|
+
update: { body: string },
|
|
88
|
+
context: CommentsApiContext,
|
|
89
|
+
) => Promise<void> | void;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Called after a comment is successfully edited.
|
|
93
|
+
*/
|
|
94
|
+
onAfterEdit?: (
|
|
95
|
+
comment: Comment,
|
|
96
|
+
context: CommentsApiContext,
|
|
97
|
+
) => Promise<void> | void;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Called before a like is toggled. Throw to reject.
|
|
101
|
+
*
|
|
102
|
+
* When this hook is **absent**, any like/unlike request is automatically
|
|
103
|
+
* rejected with 403 — preventing unauthenticated callers from toggling likes
|
|
104
|
+
* on behalf of arbitrary user IDs. Configure this hook to verify `authorId`
|
|
105
|
+
* matches the authenticated session.
|
|
106
|
+
*/
|
|
107
|
+
onBeforeLike?: (
|
|
108
|
+
commentId: string,
|
|
109
|
+
authorId: string,
|
|
110
|
+
context: CommentsApiContext,
|
|
111
|
+
) => Promise<void> | void;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Called before a comment's status is changed. Throw to reject.
|
|
115
|
+
*
|
|
116
|
+
* When this hook is **absent**, any status-change request is automatically
|
|
117
|
+
* rejected with 403 — preventing unauthenticated callers from moderating
|
|
118
|
+
* comments. Configure this hook to verify the caller has admin/moderator
|
|
119
|
+
* privileges.
|
|
120
|
+
*/
|
|
121
|
+
onBeforeStatusChange?: (
|
|
122
|
+
commentId: string,
|
|
123
|
+
status: "pending" | "approved" | "spam",
|
|
124
|
+
context: CommentsApiContext,
|
|
125
|
+
) => Promise<void> | void;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Called after a comment status is changed to "approved".
|
|
129
|
+
*/
|
|
130
|
+
onAfterApprove?: (
|
|
131
|
+
comment: Comment,
|
|
132
|
+
context: CommentsApiContext,
|
|
133
|
+
) => Promise<void> | void;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Called before a comment is deleted. Throw to reject.
|
|
137
|
+
*
|
|
138
|
+
* When this hook is **absent**, any delete request is automatically rejected
|
|
139
|
+
* with 403 — preventing unauthenticated callers from deleting comments.
|
|
140
|
+
* Configure this hook to enforce admin-only access.
|
|
141
|
+
*/
|
|
142
|
+
onBeforeDelete?: (
|
|
143
|
+
commentId: string,
|
|
144
|
+
context: CommentsApiContext,
|
|
145
|
+
) => Promise<void> | void;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Called after a comment is deleted.
|
|
149
|
+
*/
|
|
150
|
+
onAfterDelete?: (
|
|
151
|
+
commentId: string,
|
|
152
|
+
context: CommentsApiContext,
|
|
153
|
+
) => Promise<void> | void;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Called before the comment list is returned for an author-scoped query
|
|
157
|
+
* (i.e. when `authorId` is present in `GET /comments`). Throw to reject.
|
|
158
|
+
*
|
|
159
|
+
* When this hook is **absent**, any request that includes `authorId` is
|
|
160
|
+
* automatically rejected with 403 — preventing anonymous callers from
|
|
161
|
+
* reading or probing any user's comment history.
|
|
162
|
+
*/
|
|
163
|
+
onBeforeListByAuthor?: (
|
|
164
|
+
authorId: string,
|
|
165
|
+
query: z.infer<typeof CommentListQuerySchema>,
|
|
166
|
+
context: CommentsApiContext,
|
|
167
|
+
) => Promise<void> | void;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Configuration options for the comments backend plugin.
|
|
172
|
+
*
|
|
173
|
+
* TypeScript enforces the security-critical hooks based on `allowPosting`:
|
|
174
|
+
* - When `allowPosting` is absent or `true`, `onBeforePost` and
|
|
175
|
+
* `resolveCurrentUserId` are **required**.
|
|
176
|
+
* - When `allowPosting` is `false`, both become optional (the POST endpoint
|
|
177
|
+
* is not registered so neither hook is ever called).
|
|
178
|
+
*/
|
|
179
|
+
export type CommentsBackendOptions = CommentsBackendOptionsBase &
|
|
180
|
+
(
|
|
181
|
+
| {
|
|
182
|
+
/**
|
|
183
|
+
* Posting is enabled (default). `onBeforePost` and `resolveCurrentUserId`
|
|
184
|
+
* are required to prevent anonymous authorship and impersonation.
|
|
185
|
+
*/
|
|
186
|
+
allowPosting?: true;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Called before a comment is created. Must return `{ authorId: string }` —
|
|
190
|
+
* the server-resolved identity of the commenter.
|
|
191
|
+
*
|
|
192
|
+
* ⚠️ SECURITY REQUIRED: Derive `authorId` from the authenticated session
|
|
193
|
+
* (e.g. JWT / session cookie). Never trust any ID supplied by the client.
|
|
194
|
+
* Throw to reject the request (e.g. when the user is not authenticated).
|
|
195
|
+
*
|
|
196
|
+
* `authorId` is intentionally absent from the POST body schema. This hook
|
|
197
|
+
* is the only place it can be set.
|
|
198
|
+
*/
|
|
199
|
+
onBeforePost: (
|
|
200
|
+
input: z.infer<typeof createCommentSchema>,
|
|
201
|
+
context: CommentsApiContext,
|
|
202
|
+
) => Promise<{ authorId: string }> | { authorId: string };
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve the current authenticated user's ID from the request context
|
|
206
|
+
* (e.g. session cookie or JWT). Used to include the user's own pending
|
|
207
|
+
* comments alongside approved ones in `GET /comments` responses so they
|
|
208
|
+
* remain visible immediately after posting.
|
|
209
|
+
*
|
|
210
|
+
* Return `null` or `undefined` for unauthenticated requests.
|
|
211
|
+
*
|
|
212
|
+
* ```ts
|
|
213
|
+
* resolveCurrentUserId: async (ctx) => {
|
|
214
|
+
* const session = await getSession(ctx.headers)
|
|
215
|
+
* return session?.user?.id ?? null
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
resolveCurrentUserId: (
|
|
220
|
+
context: CommentsApiContext,
|
|
221
|
+
) => Promise<string | null | undefined> | string | null | undefined;
|
|
222
|
+
}
|
|
223
|
+
| {
|
|
224
|
+
/**
|
|
225
|
+
* When `false`, the `POST /comments` endpoint is not registered.
|
|
226
|
+
* No new comments or replies can be submitted — users can only read
|
|
227
|
+
* existing comments. `onBeforePost` and `resolveCurrentUserId` become
|
|
228
|
+
* optional because they are never called.
|
|
229
|
+
*/
|
|
230
|
+
allowPosting: false;
|
|
231
|
+
onBeforePost?: (
|
|
232
|
+
input: z.infer<typeof createCommentSchema>,
|
|
233
|
+
context: CommentsApiContext,
|
|
234
|
+
) => Promise<{ authorId: string }> | { authorId: string };
|
|
235
|
+
resolveCurrentUserId?: (
|
|
236
|
+
context: CommentsApiContext,
|
|
237
|
+
) => Promise<string | null | undefined> | string | null | undefined;
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
export const commentsBackendPlugin = (options: CommentsBackendOptions) => {
|
|
242
|
+
const postingEnabled = options.allowPosting !== false;
|
|
243
|
+
const editingEnabled = options.allowEditing !== false;
|
|
244
|
+
|
|
245
|
+
// Narrow once so closures below see fully-typed (non-optional) hooks.
|
|
246
|
+
// TypeScript resolves onBeforePost / resolveCurrentUserId as required in
|
|
247
|
+
// the allowPosting?: true branch, so these will be Hook | undefined — but
|
|
248
|
+
// we only call them when postingEnabled is true.
|
|
249
|
+
const onBeforePost =
|
|
250
|
+
options.allowPosting !== false ? options.onBeforePost : undefined;
|
|
251
|
+
const resolveCurrentUserId =
|
|
252
|
+
options.allowPosting !== false ? options.resolveCurrentUserId : undefined;
|
|
253
|
+
|
|
254
|
+
return defineBackendPlugin({
|
|
255
|
+
name: "comments",
|
|
256
|
+
dbPlugin: dbSchema,
|
|
257
|
+
|
|
258
|
+
api: (adapter: Adapter) => ({
|
|
259
|
+
listComments: (params: z.infer<typeof CommentListParamsSchema>) =>
|
|
260
|
+
listComments(adapter, params, options?.resolveUser),
|
|
261
|
+
getCommentById: (id: string, currentUserId?: string) =>
|
|
262
|
+
getCommentById(adapter, id, options?.resolveUser, currentUserId),
|
|
263
|
+
getCommentCount: (params: z.infer<typeof CommentCountQuerySchema>) =>
|
|
264
|
+
getCommentCount(adapter, params),
|
|
265
|
+
}),
|
|
266
|
+
|
|
267
|
+
routes: (adapter: Adapter) => {
|
|
268
|
+
// GET /comments
|
|
269
|
+
const listCommentsEndpoint = createEndpoint(
|
|
270
|
+
"/comments",
|
|
271
|
+
{
|
|
272
|
+
method: "GET",
|
|
273
|
+
query: CommentListQuerySchema,
|
|
274
|
+
},
|
|
275
|
+
async (ctx) => {
|
|
276
|
+
const context: CommentsApiContext = {
|
|
277
|
+
query: ctx.query,
|
|
278
|
+
request: ctx.request,
|
|
279
|
+
headers: ctx.headers,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (ctx.query.authorId) {
|
|
283
|
+
if (!options?.onBeforeListByAuthor) {
|
|
284
|
+
throw ctx.error(403, {
|
|
285
|
+
message:
|
|
286
|
+
"Forbidden: authorId filter requires onBeforeListByAuthor hook",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
await runHookWithShim(
|
|
290
|
+
() =>
|
|
291
|
+
options.onBeforeListByAuthor!(
|
|
292
|
+
ctx.query.authorId!,
|
|
293
|
+
ctx.query,
|
|
294
|
+
context,
|
|
295
|
+
),
|
|
296
|
+
ctx.error,
|
|
297
|
+
"Forbidden: Cannot list comments for this author",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (ctx.query.status && ctx.query.status !== "approved") {
|
|
302
|
+
if (!options?.onBeforeList) {
|
|
303
|
+
throw ctx.error(403, {
|
|
304
|
+
message: "Forbidden: status filter requires authorization",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
await runHookWithShim(
|
|
308
|
+
() => options.onBeforeList!(ctx.query, context),
|
|
309
|
+
ctx.error,
|
|
310
|
+
"Forbidden: Cannot list comments with this status filter",
|
|
311
|
+
);
|
|
312
|
+
} else if (options?.onBeforeList && !ctx.query.authorId) {
|
|
313
|
+
await runHookWithShim(
|
|
314
|
+
() => options.onBeforeList!(ctx.query, context),
|
|
315
|
+
ctx.error,
|
|
316
|
+
"Forbidden: Cannot list comments",
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let resolvedCurrentUserId: string | undefined;
|
|
321
|
+
if (resolveCurrentUserId) {
|
|
322
|
+
try {
|
|
323
|
+
const result = await resolveCurrentUserId(context);
|
|
324
|
+
resolvedCurrentUserId = result ?? undefined;
|
|
325
|
+
} catch {
|
|
326
|
+
resolvedCurrentUserId = undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return await listComments(
|
|
331
|
+
adapter,
|
|
332
|
+
{ ...ctx.query, currentUserId: resolvedCurrentUserId },
|
|
333
|
+
options?.resolveUser,
|
|
334
|
+
);
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// POST /comments
|
|
339
|
+
const createCommentEndpoint = createEndpoint(
|
|
340
|
+
"/comments",
|
|
341
|
+
{
|
|
342
|
+
method: "POST",
|
|
343
|
+
body: createCommentSchema,
|
|
344
|
+
},
|
|
345
|
+
async (ctx) => {
|
|
346
|
+
if (!postingEnabled) {
|
|
347
|
+
throw ctx.error(403, { message: "Posting comments is disabled" });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const context: CommentsApiContext = {
|
|
351
|
+
body: ctx.body,
|
|
352
|
+
headers: ctx.headers,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const { authorId } = await runHookWithShim(
|
|
356
|
+
() => onBeforePost!(ctx.body, context),
|
|
357
|
+
ctx.error,
|
|
358
|
+
"Unauthorized: Cannot post comment",
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const status = options?.autoApprove ? "approved" : "pending";
|
|
362
|
+
const comment = await createComment(adapter, {
|
|
363
|
+
...ctx.body,
|
|
364
|
+
authorId,
|
|
365
|
+
status,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (options?.onAfterPost) {
|
|
369
|
+
await options.onAfterPost(comment, context);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const serialized = await getCommentById(
|
|
373
|
+
adapter,
|
|
374
|
+
comment.id,
|
|
375
|
+
options?.resolveUser,
|
|
376
|
+
);
|
|
377
|
+
if (!serialized) {
|
|
378
|
+
throw ctx.error(500, {
|
|
379
|
+
message: "Failed to retrieve created comment",
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return serialized;
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// PATCH /comments/:id (edit body)
|
|
387
|
+
const updateCommentEndpoint = createEndpoint(
|
|
388
|
+
"/comments/:id",
|
|
389
|
+
{
|
|
390
|
+
method: "PATCH",
|
|
391
|
+
body: updateCommentSchema,
|
|
392
|
+
},
|
|
393
|
+
async (ctx) => {
|
|
394
|
+
if (!editingEnabled) {
|
|
395
|
+
throw ctx.error(403, { message: "Editing comments is disabled" });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const { id } = ctx.params;
|
|
399
|
+
const context: CommentsApiContext = {
|
|
400
|
+
params: ctx.params,
|
|
401
|
+
body: ctx.body,
|
|
402
|
+
headers: ctx.headers,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
if (!options?.onBeforeEdit) {
|
|
406
|
+
throw ctx.error(403, {
|
|
407
|
+
message:
|
|
408
|
+
"Forbidden: editing comments requires the onBeforeEdit hook",
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
await runHookWithShim(
|
|
412
|
+
() => options.onBeforeEdit!(id, { body: ctx.body.body }, context),
|
|
413
|
+
ctx.error,
|
|
414
|
+
"Unauthorized: Cannot edit comment",
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const updated = await updateComment(adapter, id, ctx.body.body);
|
|
418
|
+
if (!updated) {
|
|
419
|
+
throw ctx.error(404, { message: "Comment not found" });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (options?.onAfterEdit) {
|
|
423
|
+
await options.onAfterEdit(updated, context);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const serialized = await getCommentById(
|
|
427
|
+
adapter,
|
|
428
|
+
updated.id,
|
|
429
|
+
options?.resolveUser,
|
|
430
|
+
);
|
|
431
|
+
if (!serialized) {
|
|
432
|
+
throw ctx.error(500, {
|
|
433
|
+
message: "Failed to retrieve updated comment",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return serialized;
|
|
437
|
+
},
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// GET /comments/count
|
|
441
|
+
const getCommentCountEndpoint = createEndpoint(
|
|
442
|
+
"/comments/count",
|
|
443
|
+
{
|
|
444
|
+
method: "GET",
|
|
445
|
+
query: CommentCountQuerySchema,
|
|
446
|
+
},
|
|
447
|
+
async (ctx) => {
|
|
448
|
+
const context: CommentsApiContext = {
|
|
449
|
+
query: ctx.query,
|
|
450
|
+
headers: ctx.headers,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
if (ctx.query.status && ctx.query.status !== "approved") {
|
|
454
|
+
if (!options?.onBeforeList) {
|
|
455
|
+
throw ctx.error(403, {
|
|
456
|
+
message: "Forbidden: status filter requires authorization",
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
await runHookWithShim(
|
|
460
|
+
() =>
|
|
461
|
+
options.onBeforeList!(
|
|
462
|
+
{ ...ctx.query, status: ctx.query.status },
|
|
463
|
+
context,
|
|
464
|
+
),
|
|
465
|
+
ctx.error,
|
|
466
|
+
"Forbidden: Cannot count comments with this status filter",
|
|
467
|
+
);
|
|
468
|
+
} else if (options?.onBeforeList) {
|
|
469
|
+
await runHookWithShim(
|
|
470
|
+
() =>
|
|
471
|
+
options.onBeforeList!(
|
|
472
|
+
{ ...ctx.query, status: ctx.query.status },
|
|
473
|
+
context,
|
|
474
|
+
),
|
|
475
|
+
ctx.error,
|
|
476
|
+
"Forbidden: Cannot count comments",
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const count = await getCommentCount(adapter, ctx.query);
|
|
481
|
+
return { count };
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// POST /comments/:id/like (toggle)
|
|
486
|
+
const toggleLikeEndpoint = createEndpoint(
|
|
487
|
+
"/comments/:id/like",
|
|
488
|
+
{
|
|
489
|
+
method: "POST",
|
|
490
|
+
body: z.object({ authorId: z.string().min(1) }),
|
|
491
|
+
},
|
|
492
|
+
async (ctx) => {
|
|
493
|
+
const { id } = ctx.params;
|
|
494
|
+
const context: CommentsApiContext = {
|
|
495
|
+
params: ctx.params,
|
|
496
|
+
body: ctx.body,
|
|
497
|
+
headers: ctx.headers,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
if (!options?.onBeforeLike) {
|
|
501
|
+
throw ctx.error(403, {
|
|
502
|
+
message:
|
|
503
|
+
"Forbidden: toggling likes requires the onBeforeLike hook",
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
await runHookWithShim(
|
|
507
|
+
() => options.onBeforeLike!(id, ctx.body.authorId, context),
|
|
508
|
+
ctx.error,
|
|
509
|
+
"Unauthorized: Cannot like comment",
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const result = await toggleCommentLike(
|
|
513
|
+
adapter,
|
|
514
|
+
id,
|
|
515
|
+
ctx.body.authorId,
|
|
516
|
+
);
|
|
517
|
+
return result;
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// PATCH /comments/:id/status (admin)
|
|
522
|
+
const updateStatusEndpoint = createEndpoint(
|
|
523
|
+
"/comments/:id/status",
|
|
524
|
+
{
|
|
525
|
+
method: "PATCH",
|
|
526
|
+
body: updateCommentStatusSchema,
|
|
527
|
+
},
|
|
528
|
+
async (ctx) => {
|
|
529
|
+
const { id } = ctx.params;
|
|
530
|
+
const context: CommentsApiContext = {
|
|
531
|
+
params: ctx.params,
|
|
532
|
+
body: ctx.body,
|
|
533
|
+
headers: ctx.headers,
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
if (!options?.onBeforeStatusChange) {
|
|
537
|
+
throw ctx.error(403, {
|
|
538
|
+
message:
|
|
539
|
+
"Forbidden: changing comment status requires the onBeforeStatusChange hook",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
await runHookWithShim(
|
|
543
|
+
() => options.onBeforeStatusChange!(id, ctx.body.status, context),
|
|
544
|
+
ctx.error,
|
|
545
|
+
"Unauthorized: Cannot change comment status",
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
const updated = await updateCommentStatus(
|
|
549
|
+
adapter,
|
|
550
|
+
id,
|
|
551
|
+
ctx.body.status,
|
|
552
|
+
);
|
|
553
|
+
if (!updated) {
|
|
554
|
+
throw ctx.error(404, { message: "Comment not found" });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (ctx.body.status === "approved" && options?.onAfterApprove) {
|
|
558
|
+
await options.onAfterApprove(updated, context);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const serialized = await getCommentById(
|
|
562
|
+
adapter,
|
|
563
|
+
updated.id,
|
|
564
|
+
options?.resolveUser,
|
|
565
|
+
);
|
|
566
|
+
if (!serialized) {
|
|
567
|
+
throw ctx.error(500, {
|
|
568
|
+
message: "Failed to retrieve updated comment",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return serialized;
|
|
572
|
+
},
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
// DELETE /comments/:id (admin)
|
|
576
|
+
const deleteCommentEndpoint = createEndpoint(
|
|
577
|
+
"/comments/:id",
|
|
578
|
+
{
|
|
579
|
+
method: "DELETE",
|
|
580
|
+
},
|
|
581
|
+
async (ctx) => {
|
|
582
|
+
const { id } = ctx.params;
|
|
583
|
+
const context: CommentsApiContext = {
|
|
584
|
+
params: ctx.params,
|
|
585
|
+
headers: ctx.headers,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
if (!options?.onBeforeDelete) {
|
|
589
|
+
throw ctx.error(403, {
|
|
590
|
+
message:
|
|
591
|
+
"Forbidden: deleting comments requires the onBeforeDelete hook",
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
await runHookWithShim(
|
|
595
|
+
() => options.onBeforeDelete!(id, context),
|
|
596
|
+
ctx.error,
|
|
597
|
+
"Unauthorized: Cannot delete comment",
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const deleted = await deleteComment(adapter, id);
|
|
601
|
+
if (!deleted) {
|
|
602
|
+
throw ctx.error(404, { message: "Comment not found" });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (options?.onAfterDelete) {
|
|
606
|
+
await options.onAfterDelete(id, context);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return { success: true };
|
|
610
|
+
},
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
listComments: listCommentsEndpoint,
|
|
615
|
+
...(postingEnabled && { createComment: createCommentEndpoint }),
|
|
616
|
+
...(editingEnabled && { updateComment: updateCommentEndpoint }),
|
|
617
|
+
getCommentCount: getCommentCountEndpoint,
|
|
618
|
+
toggleLike: toggleLikeEndpoint,
|
|
619
|
+
updateCommentStatus: updateStatusEndpoint,
|
|
620
|
+
deleteComment: deleteCommentEndpoint,
|
|
621
|
+
} as const;
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
export type CommentsApiRouter = ReturnType<
|
|
627
|
+
ReturnType<typeof commentsBackendPlugin>["routes"]
|
|
628
|
+
>;
|