@btst/stack 2.4.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 (136) hide show
  1. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +33 -47
  2. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +33 -47
  3. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +14 -21
  4. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
  5. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +28 -45
  6. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
  7. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +23 -27
  8. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
  9. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +14 -17
  10. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +14 -17
  11. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
  12. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
  13. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
  14. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
  15. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
  16. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
  17. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +86 -117
  18. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +83 -114
  19. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +22 -29
  20. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
  21. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +8 -8
  22. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
  23. package/dist/packages/stack/src/plugins/utils.cjs +42 -0
  24. package/dist/packages/stack/src/plugins/utils.mjs +41 -1
  25. package/dist/plugins/ai-chat/api/index.d.cts +1 -1
  26. package/dist/plugins/ai-chat/api/index.d.mts +1 -1
  27. package/dist/plugins/ai-chat/api/index.d.ts +1 -1
  28. package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
  29. package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
  30. package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
  31. package/dist/plugins/ai-chat/client/index.d.cts +8 -8
  32. package/dist/plugins/ai-chat/client/index.d.mts +8 -8
  33. package/dist/plugins/ai-chat/client/index.d.ts +8 -8
  34. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  35. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  36. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  37. package/dist/plugins/blog/api/index.d.cts +1 -1
  38. package/dist/plugins/blog/api/index.d.mts +1 -1
  39. package/dist/plugins/blog/api/index.d.ts +1 -1
  40. package/dist/plugins/blog/client/index.d.cts +12 -12
  41. package/dist/plugins/blog/client/index.d.mts +12 -12
  42. package/dist/plugins/blog/client/index.d.ts +12 -12
  43. package/dist/plugins/blog/query-keys.d.cts +1 -1
  44. package/dist/plugins/blog/query-keys.d.mts +1 -1
  45. package/dist/plugins/blog/query-keys.d.ts +1 -1
  46. package/dist/plugins/client/index.cjs +1 -0
  47. package/dist/plugins/client/index.d.cts +8 -1
  48. package/dist/plugins/client/index.d.mts +8 -1
  49. package/dist/plugins/client/index.d.ts +8 -1
  50. package/dist/plugins/client/index.mjs +1 -1
  51. package/dist/plugins/cms/api/index.d.cts +2 -2
  52. package/dist/plugins/cms/api/index.d.mts +2 -2
  53. package/dist/plugins/cms/api/index.d.ts +2 -2
  54. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  55. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  56. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  57. package/dist/plugins/cms/client/index.d.cts +6 -6
  58. package/dist/plugins/cms/client/index.d.mts +6 -6
  59. package/dist/plugins/cms/client/index.d.ts +6 -6
  60. package/dist/plugins/cms/query-keys.d.cts +2 -2
  61. package/dist/plugins/cms/query-keys.d.mts +2 -2
  62. package/dist/plugins/cms/query-keys.d.ts +2 -2
  63. package/dist/plugins/form-builder/api/index.d.cts +2 -2
  64. package/dist/plugins/form-builder/api/index.d.mts +2 -2
  65. package/dist/plugins/form-builder/api/index.d.ts +2 -2
  66. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  67. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  68. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  69. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  70. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  71. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  72. package/dist/plugins/form-builder/client/index.d.cts +6 -6
  73. package/dist/plugins/form-builder/client/index.d.mts +6 -6
  74. package/dist/plugins/form-builder/client/index.d.ts +6 -6
  75. package/dist/plugins/form-builder/query-keys.d.cts +2 -2
  76. package/dist/plugins/form-builder/query-keys.d.mts +2 -2
  77. package/dist/plugins/form-builder/query-keys.d.ts +2 -2
  78. package/dist/plugins/kanban/api/index.d.cts +1 -1
  79. package/dist/plugins/kanban/api/index.d.mts +1 -1
  80. package/dist/plugins/kanban/api/index.d.ts +1 -1
  81. package/dist/plugins/kanban/client/index.d.cts +12 -12
  82. package/dist/plugins/kanban/client/index.d.mts +12 -12
  83. package/dist/plugins/kanban/client/index.d.ts +12 -12
  84. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  85. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  86. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  87. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  88. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  89. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  90. package/dist/plugins/ui-builder/client/index.d.cts +3 -3
  91. package/dist/plugins/ui-builder/client/index.d.mts +3 -3
  92. package/dist/plugins/ui-builder/client/index.d.ts +3 -3
  93. package/dist/plugins/ui-builder/index.d.cts +2 -2
  94. package/dist/plugins/ui-builder/index.d.mts +2 -2
  95. package/dist/plugins/ui-builder/index.d.ts +2 -2
  96. package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
  97. package/dist/shared/{stack.CczspVn2.d.mts → stack.B58oHdqm.d.mts} +1 -1
  98. package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
  99. package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
  100. package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
  101. package/dist/shared/{stack.Kq2-QzOC.d.ts → stack.BDVEpue1.d.ts} +2 -2
  102. package/dist/shared/{stack.B7ONvlD_.d.mts → stack.BTvbxZvw.d.cts} +2 -2
  103. package/dist/shared/{stack.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
  104. package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.mts} +19 -19
  105. package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.ts} +19 -19
  106. package/dist/shared/{stack.BUkC2EsZ.d.cts → stack.C9Mg2Q46.d.cts} +1 -1
  107. package/dist/shared/{stack.BEn34wW6.d.ts → stack.CTDVxbrA.d.ts} +12 -12
  108. package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
  109. package/dist/shared/{stack.BepFXT3w.d.mts → stack.CxaFNQCV.d.mts} +25 -25
  110. package/dist/shared/{stack.DWoCZff7.d.cts → stack.D-b5zbPm.d.cts} +12 -12
  111. package/dist/shared/{stack.kcdnD4gA.d.cts → stack.DTtmJPQO.d.mts} +2 -2
  112. package/dist/shared/{stack.CL8ts1Mu.d.ts → stack.DXnclTG7.d.ts} +8 -8
  113. package/dist/shared/{stack.heOA9gzA.d.cts → stack.DaZM10cp.d.cts} +8 -8
  114. package/dist/shared/{stack.DTDxgFj8.d.mts → stack.FVWf2JhZ.d.mts} +12 -12
  115. package/dist/shared/{stack.Dk5r4W1F.d.mts → stack.cfCkioTe.d.mts} +8 -8
  116. package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
  117. package/dist/shared/{stack.CgWzG5jH.d.ts → stack.j75TpKh2.d.ts} +25 -25
  118. package/dist/shared/{stack.D3GB6wKv.d.cts → stack.n1_i1p2B.d.cts} +25 -25
  119. package/dist/shared/{stack.DASmUVjX.d.ts → stack.sO33ZDhK.d.ts} +1 -1
  120. package/package.json +1 -1
  121. package/src/plugins/ai-chat/api/plugin.ts +48 -63
  122. package/src/plugins/ai-chat/client/plugin.tsx +23 -31
  123. package/src/plugins/blog/api/plugin.ts +31 -47
  124. package/src/plugins/blog/client/plugin.tsx +36 -39
  125. package/src/plugins/client/index.ts +5 -1
  126. package/src/plugins/cms/api/plugin.ts +14 -17
  127. package/src/plugins/cms/client/plugin.tsx +18 -21
  128. package/src/plugins/cms/types.ts +7 -7
  129. package/src/plugins/form-builder/api/plugin.ts +64 -64
  130. package/src/plugins/form-builder/client/plugin.tsx +19 -18
  131. package/src/plugins/form-builder/types.ts +19 -24
  132. package/src/plugins/kanban/api/plugin.ts +111 -136
  133. package/src/plugins/kanban/client/plugin.tsx +35 -41
  134. package/src/plugins/ui-builder/client/plugin.tsx +11 -10
  135. package/src/plugins/ui-builder/types.ts +4 -4
  136. package/src/plugins/utils.ts +92 -1
@@ -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";
@@ -33,6 +33,7 @@ import {
33
33
  import { createCMSContentItem } from "./mutations";
34
34
  import type { QueryClient } from "@tanstack/react-query";
35
35
  import { CMS_QUERY_KEYS } from "./query-key-defs";
36
+ import { runHookWithShim } from "../../utils";
36
37
 
37
38
  /**
38
39
  * Route keys for the CMS plugin — matches the keys returned by
@@ -779,13 +780,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
779
780
  // Call before hook - may deny operation
780
781
  const processedData = validation.data as Record<string, unknown>;
781
782
  if (config.hooks?.onBeforeCreate) {
782
- const result = await config.hooks.onBeforeCreate(
783
- processedData,
784
- context,
783
+ await runHookWithShim(
784
+ () => config.hooks!.onBeforeCreate!(processedData, context),
785
+ ctx.error,
786
+ "Create operation denied",
785
787
  );
786
- if (result === false) {
787
- throw ctx.error(403, { message: "Create operation denied" });
788
- }
789
788
  }
790
789
 
791
790
  const item = await adapter.create<ContentItem>({
@@ -922,14 +921,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
922
921
  // Call before hook - may deny operation
923
922
  const processedData = validatedData;
924
923
  if (config.hooks?.onBeforeUpdate && validatedData) {
925
- const result = await config.hooks.onBeforeUpdate(
926
- id,
927
- validatedData,
928
- context,
924
+ await runHookWithShim(
925
+ () => config.hooks!.onBeforeUpdate!(id, validatedData, context),
926
+ ctx.error,
927
+ "Update operation denied",
929
928
  );
930
- if (result === false) {
931
- throw ctx.error(403, { message: "Update operation denied" });
932
- }
933
929
  }
934
930
 
935
931
  // Sync relations to junction table if data was updated
@@ -996,10 +992,11 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
996
992
 
997
993
  // Call before hook
998
994
  if (config.hooks?.onBeforeDelete) {
999
- const canDelete = await config.hooks.onBeforeDelete(id, context);
1000
- if (!canDelete) {
1001
- throw ctx.error(403, { message: "Delete operation denied" });
1002
- }
995
+ await runHookWithShim(
996
+ () => config.hooks!.onBeforeDelete!(id, context),
997
+ ctx.error,
998
+ "Delete operation denied",
999
+ );
1003
1000
  }
1004
1001
 
1005
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;
@@ -32,6 +32,7 @@ import {
32
32
  } from "./getters";
33
33
  import { FORM_QUERY_KEYS } from "./query-key-defs";
34
34
  import type { QueryClient } from "@tanstack/react-query";
35
+ import { runHookWithShim } from "../../utils";
35
36
 
36
37
  /**
37
38
  * Route keys for the Form Builder plugin — matches the keys returned by
@@ -177,10 +178,11 @@ export const formBuilderBackendPlugin = (
177
178
  const context = createContext(ctx.headers);
178
179
 
179
180
  if (config.hooks?.onBeforeListForms) {
180
- const canList = await config.hooks.onBeforeListForms(context);
181
- if (!canList) {
182
- throw ctx.error(403, { message: "Access denied" });
183
- }
181
+ await runHookWithShim(
182
+ () => config.hooks!.onBeforeListForms!(context),
183
+ ctx.error,
184
+ "Access denied",
185
+ );
184
186
  }
185
187
 
186
188
  return getAllForms(adapter, { status, limit, offset });
@@ -199,10 +201,11 @@ export const formBuilderBackendPlugin = (
199
201
 
200
202
  // Call before hook for access check
201
203
  if (config.hooks?.onBeforeGetForm) {
202
- const canGet = await config.hooks.onBeforeGetForm(slug, context);
203
- if (!canGet) {
204
- throw ctx.error(403, { message: "Access denied" });
205
- }
204
+ await runHookWithShim(
205
+ () => config.hooks!.onBeforeGetForm!(slug, context),
206
+ ctx.error,
207
+ "Access denied",
208
+ );
206
209
  }
207
210
 
208
211
  const form = await getFormBySlugFromDb(adapter, slug);
@@ -227,10 +230,11 @@ export const formBuilderBackendPlugin = (
227
230
 
228
231
  // Call before hook for access check
229
232
  if (config.hooks?.onBeforeGetForm) {
230
- const canGet = await config.hooks.onBeforeGetForm(id, context);
231
- if (!canGet) {
232
- throw ctx.error(403, { message: "Access denied" });
233
- }
233
+ await runHookWithShim(
234
+ () => config.hooks!.onBeforeGetForm!(id, context),
235
+ ctx.error,
236
+ "Access denied",
237
+ );
234
238
  }
235
239
 
236
240
  const form = await adapter.findOne<Form>({
@@ -297,15 +301,13 @@ export const formBuilderBackendPlugin = (
297
301
 
298
302
  // Call before hook - may modify data or deny operation
299
303
  if (config.hooks?.onBeforeFormCreated) {
300
- const result = await config.hooks.onBeforeFormCreated(
301
- formInput,
302
- context,
304
+ const hookResult = await runHookWithShim(
305
+ () => config.hooks!.onBeforeFormCreated!(formInput, context),
306
+ ctx.error,
307
+ "Create operation denied",
303
308
  );
304
- if (result === false) {
305
- throw ctx.error(403, { message: "Create operation denied" });
306
- }
307
- if (result && typeof result === "object") {
308
- formInput = result;
309
+ if (hookResult && typeof hookResult === "object") {
310
+ formInput = hookResult as typeof formInput;
309
311
  }
310
312
  }
311
313
 
@@ -410,16 +412,14 @@ export const formBuilderBackendPlugin = (
410
412
 
411
413
  // Call before hook - may modify data or deny operation
412
414
  if (config.hooks?.onBeforeFormUpdated) {
413
- const result = await config.hooks.onBeforeFormUpdated(
414
- id,
415
- updateInput,
416
- context,
415
+ const hookResult = await runHookWithShim(
416
+ () =>
417
+ config.hooks!.onBeforeFormUpdated!(id, updateInput, context),
418
+ ctx.error,
419
+ "Update operation denied",
417
420
  );
418
- if (result === false) {
419
- throw ctx.error(403, { message: "Update operation denied" });
420
- }
421
- if (result && typeof result === "object") {
422
- updateInput = result;
421
+ if (hookResult && typeof hookResult === "object") {
422
+ updateInput = hookResult as typeof updateInput;
423
423
  }
424
424
  }
425
425
 
@@ -485,13 +485,11 @@ export const formBuilderBackendPlugin = (
485
485
 
486
486
  // Call before hook
487
487
  if (config.hooks?.onBeforeFormDeleted) {
488
- const canDelete = await config.hooks.onBeforeFormDeleted(
489
- id,
490
- context,
488
+ await runHookWithShim(
489
+ () => config.hooks!.onBeforeFormDeleted!(id, context),
490
+ ctx.error,
491
+ "Delete operation denied",
491
492
  );
492
- if (!canDelete) {
493
- throw ctx.error(403, { message: "Delete operation denied" });
494
- }
495
493
  }
496
494
 
497
495
  // Delete associated submissions first (cascade)
@@ -574,32 +572,40 @@ export const formBuilderBackendPlugin = (
574
572
  throw ctx.error(400, { message: "Invalid form data" });
575
573
  }
576
574
 
577
- // Call before submission hook - may modify data or deny
575
+ // Call before submission hook - may modify data or deny.
576
+ // We call the hook directly (not via runHookWithShim) so that
577
+ // onSubmissionError receives the original Error, not a wrapped HTTP error.
578
578
  let finalData = data as Record<string, unknown>;
579
579
  if (config.hooks?.onBeforeSubmission) {
580
+ let hookResult: unknown;
581
+ let originalError: Error | undefined;
580
582
  try {
581
- const result = await config.hooks.onBeforeSubmission(
583
+ hookResult = await config.hooks.onBeforeSubmission(
582
584
  slug,
583
585
  data as Record<string, unknown>,
584
586
  submissionContext,
585
587
  );
586
- if (result === false) {
587
- throw ctx.error(400, { message: "Submission rejected" });
588
- }
589
- if (result && typeof result === "object") {
590
- finalData = result;
588
+ // Backward-compat: explicit false return → denial
589
+ if (hookResult === false) {
590
+ originalError = new Error("Submission rejected");
591
591
  }
592
- } catch (error) {
593
- // Call error hook if submission is rejected
592
+ } catch (e) {
593
+ originalError =
594
+ e instanceof Error ? e : new Error("Submission rejected");
595
+ }
596
+ if (originalError) {
594
597
  if (config.hooks?.onSubmissionError) {
595
598
  await config.hooks.onSubmissionError(
596
- error as Error,
599
+ originalError,
597
600
  slug,
598
601
  data as Record<string, unknown>,
599
602
  submissionContext,
600
603
  );
601
604
  }
602
- throw error;
605
+ throw ctx.error(400, { message: originalError.message });
606
+ }
607
+ if (hookResult && typeof hookResult === "object") {
608
+ finalData = hookResult as Record<string, unknown>;
603
609
  }
604
610
  }
605
611
 
@@ -662,13 +668,11 @@ export const formBuilderBackendPlugin = (
662
668
 
663
669
  // Call before hook for auth check
664
670
  if (config.hooks?.onBeforeListSubmissions) {
665
- const canList = await config.hooks.onBeforeListSubmissions(
666
- formId,
667
- context,
671
+ await runHookWithShim(
672
+ () => config.hooks!.onBeforeListSubmissions!(formId, context),
673
+ ctx.error,
674
+ "Access denied",
668
675
  );
669
- if (!canList) {
670
- throw ctx.error(403, { message: "Access denied" });
671
- }
672
676
  }
673
677
 
674
678
  return getFormSubmissions(adapter, formId, { limit, offset });
@@ -687,13 +691,11 @@ export const formBuilderBackendPlugin = (
687
691
 
688
692
  // Call before hook for access check
689
693
  if (config.hooks?.onBeforeGetSubmission) {
690
- const canGet = await config.hooks.onBeforeGetSubmission(
691
- subId,
692
- context,
694
+ await runHookWithShim(
695
+ () => config.hooks!.onBeforeGetSubmission!(subId, context),
696
+ ctx.error,
697
+ "Access denied",
693
698
  );
694
- if (!canGet) {
695
- throw ctx.error(403, { message: "Access denied" });
696
- }
697
699
  }
698
700
 
699
701
  const submission = await adapter.findOne<FormSubmissionWithForm>({
@@ -731,13 +733,11 @@ export const formBuilderBackendPlugin = (
731
733
 
732
734
  // Call before hook
733
735
  if (config.hooks?.onBeforeSubmissionDeleted) {
734
- const canDelete = await config.hooks.onBeforeSubmissionDeleted(
735
- subId,
736
- context,
736
+ await runHookWithShim(
737
+ () => config.hooks!.onBeforeSubmissionDeleted!(subId, context),
738
+ ctx.error,
739
+ "Delete operation denied",
737
740
  );
738
- if (!canDelete) {
739
- throw ctx.error(403, { message: "Delete operation denied" });
740
- }
741
741
  }
742
742
 
743
743
  await adapter.delete({