@btst/stack 2.3.0 → 2.5.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.
Files changed (208) hide show
  1. package/dist/packages/stack/src/client/components/compose.cjs +1 -2
  2. package/dist/packages/stack/src/client/components/compose.mjs +1 -2
  3. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
  4. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
  5. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +87 -54
  6. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +87 -54
  7. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
  8. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
  9. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
  10. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
  11. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
  12. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
  13. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
  14. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
  15. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +14 -21
  16. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
  17. package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
  18. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
  19. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +28 -45
  20. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
  21. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
  22. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
  23. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
  24. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
  25. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
  26. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
  27. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
  28. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
  29. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
  30. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
  31. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +23 -27
  32. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
  33. package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
  34. package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
  35. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +21 -18
  36. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +21 -18
  37. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
  38. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
  39. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
  40. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
  41. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
  42. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
  43. package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
  44. package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
  45. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +92 -118
  46. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +89 -115
  47. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
  48. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
  49. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +22 -29
  50. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
  51. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
  52. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
  53. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +8 -8
  54. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
  55. package/dist/packages/stack/src/plugins/utils.cjs +42 -0
  56. package/dist/packages/stack/src/plugins/utils.mjs +41 -1
  57. package/dist/plugins/ai-chat/api/index.d.cts +1 -1
  58. package/dist/plugins/ai-chat/api/index.d.mts +1 -1
  59. package/dist/plugins/ai-chat/api/index.d.ts +1 -1
  60. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  61. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  62. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  63. package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
  64. package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
  65. package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
  66. package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
  67. package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
  68. package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
  69. package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
  70. package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
  71. package/dist/plugins/ai-chat/client/index.d.cts +10 -10
  72. package/dist/plugins/ai-chat/client/index.d.mts +10 -10
  73. package/dist/plugins/ai-chat/client/index.d.ts +10 -10
  74. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  75. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  76. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  77. package/dist/plugins/blog/api/index.d.cts +2 -2
  78. package/dist/plugins/blog/api/index.d.mts +2 -2
  79. package/dist/plugins/blog/api/index.d.ts +2 -2
  80. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  81. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  82. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  83. package/dist/plugins/blog/client/index.d.cts +13 -13
  84. package/dist/plugins/blog/client/index.d.mts +13 -13
  85. package/dist/plugins/blog/client/index.d.ts +13 -13
  86. package/dist/plugins/blog/query-keys.d.cts +2 -2
  87. package/dist/plugins/blog/query-keys.d.mts +2 -2
  88. package/dist/plugins/blog/query-keys.d.ts +2 -2
  89. package/dist/plugins/client/index.cjs +1 -0
  90. package/dist/plugins/client/index.d.cts +8 -1
  91. package/dist/plugins/client/index.d.mts +8 -1
  92. package/dist/plugins/client/index.d.ts +8 -1
  93. package/dist/plugins/client/index.mjs +1 -1
  94. package/dist/plugins/cms/api/index.cjs +2 -0
  95. package/dist/plugins/cms/api/index.d.cts +2 -2
  96. package/dist/plugins/cms/api/index.d.mts +2 -2
  97. package/dist/plugins/cms/api/index.d.ts +2 -2
  98. package/dist/plugins/cms/api/index.mjs +1 -0
  99. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  100. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  101. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  102. package/dist/plugins/cms/client/index.d.cts +6 -6
  103. package/dist/plugins/cms/client/index.d.mts +6 -6
  104. package/dist/plugins/cms/client/index.d.ts +6 -6
  105. package/dist/plugins/cms/query-keys.d.cts +2 -2
  106. package/dist/plugins/cms/query-keys.d.mts +2 -2
  107. package/dist/plugins/cms/query-keys.d.ts +2 -2
  108. package/dist/plugins/form-builder/api/index.d.cts +2 -2
  109. package/dist/plugins/form-builder/api/index.d.mts +2 -2
  110. package/dist/plugins/form-builder/api/index.d.ts +2 -2
  111. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  112. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  113. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  114. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  115. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  116. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  117. package/dist/plugins/form-builder/client/index.d.cts +6 -6
  118. package/dist/plugins/form-builder/client/index.d.mts +6 -6
  119. package/dist/plugins/form-builder/client/index.d.ts +6 -6
  120. package/dist/plugins/form-builder/query-keys.d.cts +2 -2
  121. package/dist/plugins/form-builder/query-keys.d.mts +2 -2
  122. package/dist/plugins/form-builder/query-keys.d.ts +2 -2
  123. package/dist/plugins/kanban/api/index.cjs +4 -0
  124. package/dist/plugins/kanban/api/index.d.cts +1 -1
  125. package/dist/plugins/kanban/api/index.d.mts +1 -1
  126. package/dist/plugins/kanban/api/index.d.ts +1 -1
  127. package/dist/plugins/kanban/api/index.mjs +1 -0
  128. package/dist/plugins/kanban/client/index.d.cts +12 -12
  129. package/dist/plugins/kanban/client/index.d.mts +12 -12
  130. package/dist/plugins/kanban/client/index.d.ts +12 -12
  131. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  132. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  133. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  134. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  135. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  136. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  137. package/dist/plugins/ui-builder/client/index.d.cts +3 -3
  138. package/dist/plugins/ui-builder/client/index.d.mts +3 -3
  139. package/dist/plugins/ui-builder/client/index.d.ts +3 -3
  140. package/dist/plugins/ui-builder/index.d.cts +2 -2
  141. package/dist/plugins/ui-builder/index.d.mts +2 -2
  142. package/dist/plugins/ui-builder/index.d.ts +2 -2
  143. package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
  144. package/dist/shared/{stack.B1EeBt1b.d.ts → stack.B58oHdqm.d.mts} +33 -3
  145. package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
  146. package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
  147. package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
  148. package/dist/shared/{stack.CIP6QS9l.d.ts → stack.BDVEpue1.d.ts} +1 -1
  149. package/dist/shared/{stack.C5dtIncc.d.mts → stack.BTvbxZvw.d.cts} +1 -1
  150. package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
  151. package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
  152. package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
  153. package/dist/shared/{stack.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
  154. package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.mts} +19 -19
  155. package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.ts} +19 -19
  156. package/dist/shared/{stack.CP68pFEH.d.mts → stack.C9Mg2Q46.d.cts} +33 -3
  157. package/dist/shared/{stack.BeSm90va.d.ts → stack.CTDVxbrA.d.ts} +72 -14
  158. package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
  159. package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CxaFNQCV.d.mts} +89 -34
  160. package/dist/shared/{stack.CMh_EdxW.d.cts → stack.D-b5zbPm.d.cts} +72 -14
  161. package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.DTtmJPQO.d.mts} +1 -1
  162. package/dist/shared/{stack.BKfolAyK.d.ts → stack.DXnclTG7.d.ts} +11 -11
  163. package/dist/shared/{stack.snB1EDP7.d.cts → stack.DaZM10cp.d.cts} +11 -11
  164. package/dist/shared/{stack.Dg09R0oB.d.mts → stack.FVWf2JhZ.d.mts} +72 -14
  165. package/dist/shared/{stack.BIXEI6v_.d.mts → stack.cfCkioTe.d.mts} +11 -11
  166. package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
  167. package/dist/shared/{stack.BpolpQpf.d.cts → stack.j75TpKh2.d.ts} +89 -34
  168. package/dist/shared/{stack.rTy7-wQU.d.mts → stack.n1_i1p2B.d.cts} +89 -34
  169. package/dist/shared/{stack.IdtKDRka.d.cts → stack.sO33ZDhK.d.ts} +33 -3
  170. package/package.json +14 -1
  171. package/src/client/components/compose.tsx +7 -4
  172. package/src/plugins/ai-chat/api/page-tools.ts +111 -0
  173. package/src/plugins/ai-chat/api/plugin.ts +228 -72
  174. package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
  175. package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
  176. package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
  177. package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
  178. package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
  179. package/src/plugins/ai-chat/client/plugin.tsx +23 -31
  180. package/src/plugins/ai-chat/schemas.ts +16 -0
  181. package/src/plugins/blog/api/plugin.ts +31 -47
  182. package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
  183. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
  184. package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
  185. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
  186. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
  187. package/src/plugins/blog/client/plugin.tsx +36 -39
  188. package/src/plugins/client/index.ts +5 -1
  189. package/src/plugins/cms/api/index.ts +4 -0
  190. package/src/plugins/cms/api/mutations.ts +84 -0
  191. package/src/plugins/cms/api/plugin.ts +23 -17
  192. package/src/plugins/cms/client/plugin.tsx +18 -21
  193. package/src/plugins/cms/types.ts +7 -7
  194. package/src/plugins/form-builder/api/plugin.ts +64 -64
  195. package/src/plugins/form-builder/client/plugin.tsx +19 -18
  196. package/src/plugins/form-builder/types.ts +19 -24
  197. package/src/plugins/kanban/api/index.ts +6 -0
  198. package/src/plugins/kanban/api/mutations.ts +169 -0
  199. package/src/plugins/kanban/api/plugin.ts +123 -136
  200. package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
  201. package/src/plugins/kanban/client/plugin.tsx +35 -41
  202. package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
  203. package/src/plugins/ui-builder/client/plugin.tsx +11 -10
  204. package/src/plugins/ui-builder/types.ts +4 -4
  205. package/src/plugins/utils.ts +92 -1
  206. package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.cts} +2 -2
  207. package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.mts} +2 -2
  208. package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.ts} +2 -2
@@ -0,0 +1,38 @@
1
+ import type { RefObject } from "react";
2
+ import type { UseFormReturn } from "react-hook-form";
3
+
4
+ /**
5
+ * Returns a `fillBlogForm` client tool handler bound to a form ref.
6
+ * Used by both the new-post and edit-post pages so the field-mapping
7
+ * logic stays in one place when the form schema changes.
8
+ */
9
+ export function createFillBlogFormHandler(
10
+ formRef: RefObject<UseFormReturn<any> | null>,
11
+ successMessage: string,
12
+ ) {
13
+ return async ({
14
+ title,
15
+ content,
16
+ excerpt,
17
+ tags,
18
+ }: {
19
+ title?: string;
20
+ content?: string;
21
+ excerpt?: string;
22
+ tags?: string[];
23
+ }) => {
24
+ const form = formRef.current;
25
+ if (!form) return { success: false, message: "Form not ready" };
26
+ if (title !== undefined)
27
+ form.setValue("title", title, { shouldValidate: true });
28
+ if (content !== undefined)
29
+ form.setValue("content", content, { shouldValidate: true });
30
+ if (excerpt !== undefined) form.setValue("excerpt", excerpt);
31
+ if (tags !== undefined)
32
+ form.setValue(
33
+ "tags",
34
+ tags.map((name: string) => ({ name })),
35
+ );
36
+ return { success: true, message: successMessage };
37
+ };
38
+ }
@@ -7,6 +7,10 @@ import { PageWrapper } from "../shared/page-wrapper";
7
7
  import type { BlogPluginOverrides } from "../../overrides";
8
8
  import { BLOG_LOCALIZATION } from "../../localization";
9
9
  import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
10
+ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
11
+ import { useRef, useCallback } from "react";
12
+ import type { UseFormReturn } from "react-hook-form";
13
+ import { createFillBlogFormHandler } from "./fill-blog-form-handler";
10
14
 
11
15
  // Internal component with actual page content
12
16
  export function NewPostPage() {
@@ -35,6 +39,30 @@ export function NewPostPage() {
35
39
  },
36
40
  });
37
41
 
42
+ // Ref to capture the form instance from AddPostForm via onFormReady callback
43
+ const formRef = useRef<UseFormReturn<any> | null>(null);
44
+ const handleFormReady = useCallback((form: UseFormReturn<any>) => {
45
+ formRef.current = form;
46
+ }, []);
47
+
48
+ // Register AI context so the chat can fill in the new post form
49
+ useRegisterPageAIContext({
50
+ routeName: "blog-new-post",
51
+ pageDescription:
52
+ "User is creating a new blog post in the admin editor. IMPORTANT: When asked to write, draft, or create a blog post, you MUST call the fillBlogForm tool to populate the form fields directly — do NOT just output the text in your response.",
53
+ suggestions: [
54
+ "Write a post about AI trends",
55
+ "Draft an intro paragraph",
56
+ "Suggest 5 tags for this post",
57
+ ],
58
+ clientTools: {
59
+ fillBlogForm: createFillBlogFormHandler(
60
+ formRef,
61
+ "Form filled successfully",
62
+ ),
63
+ },
64
+ });
65
+
38
66
  const handleClose = () => {
39
67
  navigate(`${basePath}/blog`);
40
68
  };
@@ -54,7 +82,11 @@ export function NewPostPage() {
54
82
  title={localization.BLOG_POST_ADD_TITLE}
55
83
  description={localization.BLOG_POST_ADD_DESCRIPTION}
56
84
  />
57
- <AddPostForm onClose={handleClose} onSuccess={handleSuccess} />
85
+ <AddPostForm
86
+ onClose={handleClose}
87
+ onSuccess={handleSuccess}
88
+ onFormReady={handleFormReady}
89
+ />
58
90
  </PageWrapper>
59
91
  );
60
92
  }
@@ -20,6 +20,7 @@ import { Badge } from "@workspace/ui/components/badge";
20
20
  import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
21
21
  import { OnThisPage, OnThisPageSelect } from "../shared/on-this-page";
22
22
  import type { SerializedPost } from "../../../types";
23
+ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
23
24
 
24
25
  // Internal component with actual page content
25
26
  export function PostPage({ slug }: { slug: string }) {
@@ -64,6 +65,25 @@ export function PostPage({ slug }: { slug: string }) {
64
65
  enabled: !!post,
65
66
  });
66
67
 
68
+ // Register page AI context so the chat can summarize and discuss this post
69
+ useRegisterPageAIContext(
70
+ post
71
+ ? {
72
+ routeName: "blog-post",
73
+ pageDescription:
74
+ `Blog post: "${post.title}"\nAuthor: ${post.authorId ?? "Unknown"}\n\n${post.content ?? ""}`.slice(
75
+ 0,
76
+ 16000,
77
+ ),
78
+ suggestions: [
79
+ "Summarize this post",
80
+ "What are the key takeaways?",
81
+ "Explain this in simpler terms",
82
+ ],
83
+ }
84
+ : null,
85
+ );
86
+
67
87
  if (!slug || !post) {
68
88
  return (
69
89
  <PageWrapper>
@@ -2,6 +2,7 @@ import {
2
2
  defineClientPlugin,
3
3
  createApiClient,
4
4
  isConnectionError,
5
+ runClientHookWithShim,
5
6
  } from "@btst/stack/plugins/client";
6
7
  import { createRoute } from "@btst/yar";
7
8
  import type { QueryClient } from "@tanstack/react-query";
@@ -91,16 +92,16 @@ export interface BlogClientConfig {
91
92
  */
92
93
  export interface BlogClientHooks {
93
94
  /**
94
- * Called before loading posts list. Return false to cancel loading.
95
+ * Called before loading posts list. Throw an error to cancel loading.
95
96
  * @param filter - Filter parameters including published status
96
97
  * @param context - Loader context with path, params, etc.
97
98
  */
98
99
  beforeLoadPosts?: (
99
100
  filter: { published: boolean },
100
101
  context: LoaderContext,
101
- ) => Promise<boolean> | boolean;
102
+ ) => Promise<void> | void;
102
103
  /**
103
- * Called after posts are loaded. Return false to cancel further processing.
104
+ * Called after posts are loaded. Throw an error to cancel further processing.
104
105
  * @param posts - Array of loaded posts or null
105
106
  * @param filter - Filter parameters used
106
107
  * @param context - Loader context
@@ -109,18 +110,18 @@ export interface BlogClientHooks {
109
110
  posts: Post[] | null,
110
111
  filter: { published: boolean },
111
112
  context: LoaderContext,
112
- ) => Promise<boolean> | boolean;
113
+ ) => Promise<void> | void;
113
114
  /**
114
- * Called before loading a single post. Return false to cancel loading.
115
+ * Called before loading a single post. Throw an error to cancel loading.
115
116
  * @param slug - Post slug being loaded
116
117
  * @param context - Loader context
117
118
  */
118
119
  beforeLoadPost?: (
119
120
  slug: string,
120
121
  context: LoaderContext,
121
- ) => Promise<boolean> | boolean;
122
+ ) => Promise<void> | void;
122
123
  /**
123
- * Called after a post is loaded. Return false to cancel further processing.
124
+ * Called after a post is loaded. Throw an error to cancel further processing.
124
125
  * @param post - Loaded post or null if not found
125
126
  * @param slug - Post slug that was requested
126
127
  * @param context - Loader context
@@ -129,17 +130,17 @@ export interface BlogClientHooks {
129
130
  post: Post | null,
130
131
  slug: string,
131
132
  context: LoaderContext,
132
- ) => Promise<boolean> | boolean;
133
+ ) => Promise<void> | void;
133
134
  /**
134
- * Called before loading the new post page. Return false to cancel.
135
+ * Called before loading the new post page. Throw an error to cancel.
135
136
  * @param context - Loader context
136
137
  */
137
- beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
138
+ beforeLoadNewPost?: (context: LoaderContext) => Promise<void> | void;
138
139
  /**
139
- * Called after the new post page is loaded. Return false to cancel.
140
+ * Called after the new post page is loaded. Throw an error to cancel.
140
141
  * @param context - Loader context
141
142
  */
142
- afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
143
+ afterLoadNewPost?: (context: LoaderContext) => Promise<void> | void;
143
144
  /**
144
145
  * Called when a loading error occurs
145
146
  * @param error - The error that occurred
@@ -165,10 +166,10 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
165
166
  try {
166
167
  // Before hook
167
168
  if (hooks?.beforeLoadPosts) {
168
- const canLoad = await hooks.beforeLoadPosts({ published }, context);
169
- if (!canLoad) {
170
- throw new Error("Load prevented by beforeLoadPosts hook");
171
- }
169
+ await runClientHookWithShim(
170
+ () => hooks.beforeLoadPosts!({ published }, context),
171
+ "Load prevented by beforeLoadPosts hook",
172
+ );
172
173
  }
173
174
 
174
175
  const limit = 10;
@@ -202,14 +203,10 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
202
203
  if (hooks?.afterLoadPosts) {
203
204
  const posts =
204
205
  queryClient.getQueryData<Post[]>(listQuery.queryKey) || null;
205
- const canContinue = await hooks.afterLoadPosts(
206
- posts,
207
- { published },
208
- context,
206
+ await runClientHookWithShim(
207
+ () => hooks.afterLoadPosts!(posts, { published }, context),
208
+ "Load prevented by afterLoadPosts hook",
209
209
  );
210
- if (canContinue === false) {
211
- throw new Error("Load prevented by afterLoadPosts hook");
212
- }
213
210
  }
214
211
 
215
212
  // Check if there was an error after afterLoadPosts hook
@@ -263,10 +260,10 @@ function createPostLoader(
263
260
  try {
264
261
  // Before hook
265
262
  if (hooks?.beforeLoadPost) {
266
- const canLoad = await hooks.beforeLoadPost(slug, context);
267
- if (!canLoad) {
268
- throw new Error("Load prevented by beforeLoadPost hook");
269
- }
263
+ await runClientHookWithShim(
264
+ () => hooks.beforeLoadPost!(slug, context),
265
+ "Load prevented by beforeLoadPost hook",
266
+ );
270
267
  }
271
268
 
272
269
  const client = createApiClient<BlogApiRouter>({
@@ -285,10 +282,10 @@ function createPostLoader(
285
282
  if (hooks?.afterLoadPost) {
286
283
  const post =
287
284
  queryClient.getQueryData<Post>(postQuery.queryKey) || null;
288
- const canContinue = await hooks.afterLoadPost(post, slug, context);
289
- if (canContinue === false) {
290
- throw new Error("Load prevented by afterLoadPost hook");
291
- }
285
+ await runClientHookWithShim(
286
+ () => hooks.afterLoadPost!(post, slug, context),
287
+ "Load prevented by afterLoadPost hook",
288
+ );
292
289
  }
293
290
 
294
291
  // Check if there was an error after afterLoadPost hook
@@ -337,18 +334,18 @@ function createNewPostLoader(config: BlogClientConfig) {
337
334
  try {
338
335
  // Before hook
339
336
  if (hooks?.beforeLoadNewPost) {
340
- const canLoad = await hooks.beforeLoadNewPost(context);
341
- if (!canLoad) {
342
- throw new Error("Load prevented by beforeLoadNewPost hook");
343
- }
337
+ await runClientHookWithShim(
338
+ () => hooks.beforeLoadNewPost!(context),
339
+ "Load prevented by beforeLoadNewPost hook",
340
+ );
344
341
  }
345
342
 
346
343
  // After hook
347
344
  if (hooks?.afterLoadNewPost) {
348
- const canContinue = await hooks.afterLoadNewPost(context);
349
- if (canContinue === false) {
350
- throw new Error("Load prevented by afterLoadNewPost hook");
351
- }
345
+ await runClientHookWithShim(
346
+ () => hooks.afterLoadNewPost!(context),
347
+ "Load prevented by afterLoadNewPost hook",
348
+ );
352
349
  }
353
350
  } catch (error) {
354
351
  // Error hook - log the error but don't throw during SSR
@@ -18,7 +18,11 @@ export type {
18
18
  PluginOverrides,
19
19
  } from "../../types";
20
20
 
21
- export { createApiClient, isConnectionError } from "../utils";
21
+ export {
22
+ createApiClient,
23
+ isConnectionError,
24
+ runClientHookWithShim,
25
+ } from "../utils";
22
26
 
23
27
  // Re-export Yar types needed for plugins
24
28
  export type { Route } from "@btst/yar";
@@ -12,4 +12,8 @@ export {
12
12
  serializeContentItem,
13
13
  serializeContentItemWithType,
14
14
  } from "./getters";
15
+ export {
16
+ createCMSContentItem,
17
+ type CreateCMSContentItemInput,
18
+ } from "./mutations";
15
19
  export { CMS_QUERY_KEYS } from "./query-key-defs";
@@ -0,0 +1,84 @@
1
+ import type { Adapter } from "@btst/db";
2
+ import type { ContentType, ContentItem } from "../types";
3
+ import { serializeContentItem } from "./getters";
4
+ import type { SerializedContentItem } from "../types";
5
+
6
+ /**
7
+ * Input for creating a new CMS content item.
8
+ */
9
+ export interface CreateCMSContentItemInput {
10
+ /** URL-safe slug for the item */
11
+ slug: string;
12
+ /** Arbitrary data payload — should match the content type schema */
13
+ data: Record<string, unknown>;
14
+ }
15
+
16
+ /**
17
+ * Create a new content item for a content type (looked up by slug).
18
+ *
19
+ * Bypasses Zod schema validation and relation processing — the caller is
20
+ * responsible for providing valid, relation-free data. For relation fields or
21
+ * schema validation, use the HTTP endpoint instead.
22
+ *
23
+ * Throws if the content type is not found or the slug is already taken within
24
+ * that content type.
25
+ *
26
+ * @remarks **Security:** No authorization hooks (`onBeforeCreate`, `onAfterCreate`)
27
+ * are called. The caller is responsible for any access-control checks before
28
+ * invoking this function.
29
+ *
30
+ * @param adapter - The database adapter
31
+ * @param contentTypeSlug - Slug of the target content type
32
+ * @param input - Item slug and data payload
33
+ */
34
+ export async function createCMSContentItem(
35
+ adapter: Adapter,
36
+ contentTypeSlug: string,
37
+ input: CreateCMSContentItemInput,
38
+ ): Promise<SerializedContentItem> {
39
+ const contentType = await adapter.findOne<ContentType>({
40
+ model: "contentType",
41
+ where: [
42
+ {
43
+ field: "slug",
44
+ value: contentTypeSlug,
45
+ operator: "eq" as const,
46
+ },
47
+ ],
48
+ });
49
+
50
+ if (!contentType) {
51
+ throw new Error(`Content type "${contentTypeSlug}" not found`);
52
+ }
53
+
54
+ const existing = await adapter.findOne<ContentItem>({
55
+ model: "contentItem",
56
+ where: [
57
+ {
58
+ field: "contentTypeId",
59
+ value: contentType.id,
60
+ operator: "eq" as const,
61
+ },
62
+ { field: "slug", value: input.slug, operator: "eq" as const },
63
+ ],
64
+ });
65
+
66
+ if (existing) {
67
+ throw new Error(
68
+ `Content item with slug "${input.slug}" already exists in type "${contentTypeSlug}"`,
69
+ );
70
+ }
71
+
72
+ const item = await adapter.create<ContentItem>({
73
+ model: "contentItem",
74
+ data: {
75
+ contentTypeId: contentType.id,
76
+ slug: input.slug,
77
+ data: JSON.stringify(input.data),
78
+ createdAt: new Date(),
79
+ updatedAt: new Date(),
80
+ },
81
+ });
82
+
83
+ return serializeContentItem(item);
84
+ }
@@ -30,8 +30,10 @@ import {
30
30
  serializeContentItem,
31
31
  serializeContentItemWithType,
32
32
  } from "./getters";
33
+ import { createCMSContentItem } from "./mutations";
33
34
  import type { QueryClient } from "@tanstack/react-query";
34
35
  import { CMS_QUERY_KEYS } from "./query-key-defs";
36
+ import { runHookWithShim } from "../../utils";
35
37
 
36
38
  /**
37
39
  * Route keys for the CMS plugin — matches the keys returned by
@@ -569,6 +571,14 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
569
571
  return getContentItemById(adapter, id);
570
572
  },
571
573
  prefetchForRoute: createCMSPrefetchForRoute(adapter),
574
+ // Mutations
575
+ createContentItem: async (
576
+ typeSlug: string,
577
+ input: Parameters<typeof createCMSContentItem>[2],
578
+ ) => {
579
+ await ensureSynced(adapter);
580
+ return createCMSContentItem(adapter, typeSlug, input);
581
+ },
572
582
  }),
573
583
 
574
584
  routes: (adapter: Adapter) => {
@@ -770,13 +780,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
770
780
  // Call before hook - may deny operation
771
781
  const processedData = validation.data as Record<string, unknown>;
772
782
  if (config.hooks?.onBeforeCreate) {
773
- const result = await config.hooks.onBeforeCreate(
774
- processedData,
775
- context,
783
+ await runHookWithShim(
784
+ () => config.hooks!.onBeforeCreate!(processedData, context),
785
+ ctx.error,
786
+ "Create operation denied",
776
787
  );
777
- if (result === false) {
778
- throw ctx.error(403, { message: "Create operation denied" });
779
- }
780
788
  }
781
789
 
782
790
  const item = await adapter.create<ContentItem>({
@@ -913,14 +921,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
913
921
  // Call before hook - may deny operation
914
922
  const processedData = validatedData;
915
923
  if (config.hooks?.onBeforeUpdate && validatedData) {
916
- const result = await config.hooks.onBeforeUpdate(
917
- id,
918
- validatedData,
919
- context,
924
+ await runHookWithShim(
925
+ () => config.hooks!.onBeforeUpdate!(id, validatedData, context),
926
+ ctx.error,
927
+ "Update operation denied",
920
928
  );
921
- if (result === false) {
922
- throw ctx.error(403, { message: "Update operation denied" });
923
- }
924
929
  }
925
930
 
926
931
  // Sync relations to junction table if data was updated
@@ -987,10 +992,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
987
992
 
988
993
  // Call before hook
989
994
  if (config.hooks?.onBeforeDelete) {
990
- const canDelete = await config.hooks.onBeforeDelete(id, context);
991
- if (!canDelete) {
992
- throw ctx.error(403, { message: "Delete operation denied" });
993
- }
995
+ await runHookWithShim(
996
+ () => config.hooks!.onBeforeDelete!(id, context),
997
+ ctx.error,
998
+ "Delete operation denied",
999
+ );
994
1000
  }
995
1001
 
996
1002
  await adapter.delete({
@@ -3,6 +3,7 @@ import {
3
3
  defineClientPlugin,
4
4
  createApiClient,
5
5
  isConnectionError,
6
+ runClientHookWithShim,
6
7
  } from "@btst/stack/plugins/client";
7
8
  import { createRoute } from "@btst/yar";
8
9
  import type { QueryClient } from "@tanstack/react-query";
@@ -52,24 +53,24 @@ export interface LoaderContext {
52
53
  */
53
54
  export interface CMSClientHooks {
54
55
  /**
55
- * Called before loading the dashboard page. Return false to cancel loading.
56
+ * Called before loading the dashboard page. Throw an error to cancel loading.
56
57
  * @param context - Loader context with path, params, etc.
57
58
  */
58
- beforeLoadDashboard?: (context: LoaderContext) => Promise<boolean> | boolean;
59
+ beforeLoadDashboard?: (context: LoaderContext) => Promise<void> | void;
59
60
  /**
60
61
  * Called after the dashboard is loaded.
61
62
  * @param context - Loader context
62
63
  */
63
64
  afterLoadDashboard?: (context: LoaderContext) => Promise<void> | void;
64
65
  /**
65
- * Called before loading a content list page. Return false to cancel loading.
66
+ * Called before loading a content list page. Throw an error to cancel loading.
66
67
  * @param typeSlug - The content type slug
67
68
  * @param context - Loader context
68
69
  */
69
70
  beforeLoadContentList?: (
70
71
  typeSlug: string,
71
72
  context: LoaderContext,
72
- ) => Promise<boolean> | boolean;
73
+ ) => Promise<void> | void;
73
74
  /**
74
75
  * Called after a content list is loaded.
75
76
  * @param typeSlug - The content type slug
@@ -80,7 +81,7 @@ export interface CMSClientHooks {
80
81
  context: LoaderContext,
81
82
  ) => Promise<void> | void;
82
83
  /**
83
- * Called before loading the content editor page. Return false to cancel loading.
84
+ * Called before loading the content editor page. Throw an error to cancel loading.
84
85
  * @param typeSlug - The content type slug
85
86
  * @param id - The content item ID (undefined for new items)
86
87
  * @param context - Loader context
@@ -89,7 +90,7 @@ export interface CMSClientHooks {
89
90
  typeSlug: string,
90
91
  id: string | undefined,
91
92
  context: LoaderContext,
92
- ) => Promise<boolean> | boolean;
93
+ ) => Promise<void> | void;
93
94
  /**
94
95
  * Called after the content editor is loaded.
95
96
  * @param typeSlug - The content type slug
@@ -149,10 +150,10 @@ function createDashboardLoader(config: CMSClientConfig) {
149
150
  try {
150
151
  // Before hook - authorization check
151
152
  if (hooks?.beforeLoadDashboard) {
152
- const canLoad = await hooks.beforeLoadDashboard(context);
153
- if (!canLoad) {
154
- throw new Error("Load prevented by beforeLoadDashboard hook");
155
- }
153
+ await runClientHookWithShim(
154
+ () => hooks.beforeLoadDashboard!(context),
155
+ "Load prevented by beforeLoadDashboard hook",
156
+ );
156
157
  }
157
158
 
158
159
  const client = createApiClient<CMSApiRouter>({
@@ -217,10 +218,10 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
217
218
  try {
218
219
  // Before hook - authorization check
219
220
  if (hooks?.beforeLoadContentList) {
220
- const canLoad = await hooks.beforeLoadContentList(typeSlug, context);
221
- if (!canLoad) {
222
- throw new Error("Load prevented by beforeLoadContentList hook");
223
- }
221
+ await runClientHookWithShim(
222
+ () => hooks.beforeLoadContentList!(typeSlug, context),
223
+ "Load prevented by beforeLoadContentList hook",
224
+ );
224
225
  }
225
226
 
226
227
  const client = createApiClient<CMSApiRouter>({
@@ -321,14 +322,10 @@ function createContentEditorLoader(
321
322
  try {
322
323
  // Before hook - authorization check
323
324
  if (hooks?.beforeLoadContentEditor) {
324
- const canLoad = await hooks.beforeLoadContentEditor(
325
- typeSlug,
326
- id,
327
- context,
325
+ await runClientHookWithShim(
326
+ () => hooks.beforeLoadContentEditor!(typeSlug, id, context),
327
+ "Load prevented by beforeLoadContentEditor hook",
328
328
  );
329
- if (!canLoad) {
330
- throw new Error("Load prevented by beforeLoadContentEditor hook");
331
- }
332
329
  }
333
330
 
334
331
  const client = createApiClient<CMSApiRouter>({
@@ -248,16 +248,16 @@ export interface CMSHookContext {
248
248
  /**
249
249
  * Hooks for customizing CMS backend behavior
250
250
  *
251
- * Note: Before hooks can only deny operations by returning `false`.
251
+ * Note: Before hooks deny operations by throwing an error.
252
252
  * They cannot modify the data being saved. This ensures consistency
253
253
  * between the stored content item data and relation junction tables.
254
254
  */
255
255
  export interface CMSBackendHooks {
256
- /** Called before creating a content item. Return false to deny the operation. */
256
+ /** Called before creating a content item. Throw an error to deny the operation. */
257
257
  onBeforeCreate?: (
258
258
  data: Record<string, unknown>,
259
259
  context: CMSHookContext,
260
- ) => Promise<false | void> | false | void;
260
+ ) => Promise<void> | void;
261
261
 
262
262
  /** Called after creating a content item */
263
263
  onAfterCreate?: (
@@ -265,12 +265,12 @@ export interface CMSBackendHooks {
265
265
  context: CMSHookContext,
266
266
  ) => Promise<void> | void;
267
267
 
268
- /** Called before updating a content item. Return false to deny the operation. */
268
+ /** Called before updating a content item. Throw an error to deny the operation. */
269
269
  onBeforeUpdate?: (
270
270
  id: string,
271
271
  data: Record<string, unknown>,
272
272
  context: CMSHookContext,
273
- ) => Promise<false | void> | false | void;
273
+ ) => Promise<void> | void;
274
274
 
275
275
  /** Called after updating a content item */
276
276
  onAfterUpdate?: (
@@ -278,11 +278,11 @@ export interface CMSBackendHooks {
278
278
  context: CMSHookContext,
279
279
  ) => Promise<void> | void;
280
280
 
281
- /** Called before deleting a content item */
281
+ /** Called before deleting a content item. Throw an error to deny the operation. */
282
282
  onBeforeDelete?: (
283
283
  id: string,
284
284
  context: CMSHookContext,
285
- ) => Promise<boolean> | boolean;
285
+ ) => Promise<void> | void;
286
286
 
287
287
  /** Called after deleting a content item */
288
288
  onAfterDelete?: (id: string, context: CMSHookContext) => Promise<void> | void;