@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.
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +33 -47
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +33 -47
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +14 -21
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
- package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +28 -45
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +23 -27
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +14 -17
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +14 -17
- package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +86 -117
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +83 -114
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +22 -29
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
- package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +8 -8
- package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
- package/dist/packages/stack/src/plugins/utils.cjs +42 -0
- package/dist/packages/stack/src/plugins/utils.mjs +41 -1
- package/dist/plugins/ai-chat/api/index.d.cts +1 -1
- package/dist/plugins/ai-chat/api/index.d.mts +1 -1
- package/dist/plugins/ai-chat/api/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/index.d.cts +8 -8
- package/dist/plugins/ai-chat/client/index.d.mts +8 -8
- package/dist/plugins/ai-chat/client/index.d.ts +8 -8
- package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/index.d.cts +12 -12
- package/dist/plugins/blog/client/index.d.mts +12 -12
- package/dist/plugins/blog/client/index.d.ts +12 -12
- package/dist/plugins/blog/query-keys.d.cts +1 -1
- package/dist/plugins/blog/query-keys.d.mts +1 -1
- package/dist/plugins/blog/query-keys.d.ts +1 -1
- package/dist/plugins/client/index.cjs +1 -0
- package/dist/plugins/client/index.d.cts +8 -1
- package/dist/plugins/client/index.d.mts +8 -1
- package/dist/plugins/client/index.d.ts +8 -1
- package/dist/plugins/client/index.mjs +1 -1
- package/dist/plugins/cms/api/index.d.cts +2 -2
- package/dist/plugins/cms/api/index.d.mts +2 -2
- package/dist/plugins/cms/api/index.d.ts +2 -2
- package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
- package/dist/plugins/cms/client/index.d.cts +6 -6
- package/dist/plugins/cms/client/index.d.mts +6 -6
- package/dist/plugins/cms/client/index.d.ts +6 -6
- package/dist/plugins/cms/query-keys.d.cts +2 -2
- package/dist/plugins/cms/query-keys.d.mts +2 -2
- package/dist/plugins/cms/query-keys.d.ts +2 -2
- 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/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/index.d.cts +6 -6
- package/dist/plugins/form-builder/client/index.d.mts +6 -6
- package/dist/plugins/form-builder/client/index.d.ts +6 -6
- package/dist/plugins/form-builder/query-keys.d.cts +2 -2
- package/dist/plugins/form-builder/query-keys.d.mts +2 -2
- package/dist/plugins/form-builder/query-keys.d.ts +2 -2
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +12 -12
- package/dist/plugins/kanban/client/index.d.mts +12 -12
- package/dist/plugins/kanban/client/index.d.ts +12 -12
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
- 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 +2 -2
- package/dist/plugins/ui-builder/index.d.mts +2 -2
- package/dist/plugins/ui-builder/index.d.ts +2 -2
- package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
- package/dist/shared/{stack.CczspVn2.d.mts → stack.B58oHdqm.d.mts} +1 -1
- package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
- package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
- package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
- package/dist/shared/{stack.Kq2-QzOC.d.ts → stack.BDVEpue1.d.ts} +2 -2
- package/dist/shared/{stack.B7ONvlD_.d.mts → stack.BTvbxZvw.d.cts} +2 -2
- package/dist/shared/{stack.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
- package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.mts} +19 -19
- package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.ts} +19 -19
- package/dist/shared/{stack.BUkC2EsZ.d.cts → stack.C9Mg2Q46.d.cts} +1 -1
- package/dist/shared/{stack.BEn34wW6.d.ts → stack.CTDVxbrA.d.ts} +12 -12
- package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
- package/dist/shared/{stack.BepFXT3w.d.mts → stack.CxaFNQCV.d.mts} +25 -25
- package/dist/shared/{stack.DWoCZff7.d.cts → stack.D-b5zbPm.d.cts} +12 -12
- package/dist/shared/{stack.kcdnD4gA.d.cts → stack.DTtmJPQO.d.mts} +2 -2
- package/dist/shared/{stack.CL8ts1Mu.d.ts → stack.DXnclTG7.d.ts} +8 -8
- package/dist/shared/{stack.heOA9gzA.d.cts → stack.DaZM10cp.d.cts} +8 -8
- package/dist/shared/{stack.DTDxgFj8.d.mts → stack.FVWf2JhZ.d.mts} +12 -12
- package/dist/shared/{stack.Dk5r4W1F.d.mts → stack.cfCkioTe.d.mts} +8 -8
- package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
- package/dist/shared/{stack.CgWzG5jH.d.ts → stack.j75TpKh2.d.ts} +25 -25
- package/dist/shared/{stack.D3GB6wKv.d.cts → stack.n1_i1p2B.d.cts} +25 -25
- package/dist/shared/{stack.DASmUVjX.d.ts → stack.sO33ZDhK.d.ts} +1 -1
- package/package.json +1 -1
- package/src/plugins/ai-chat/api/plugin.ts +48 -63
- package/src/plugins/ai-chat/client/plugin.tsx +23 -31
- package/src/plugins/blog/api/plugin.ts +31 -47
- package/src/plugins/blog/client/plugin.tsx +36 -39
- package/src/plugins/client/index.ts +5 -1
- package/src/plugins/cms/api/plugin.ts +14 -17
- package/src/plugins/cms/client/plugin.tsx +18 -21
- package/src/plugins/cms/types.ts +7 -7
- package/src/plugins/form-builder/api/plugin.ts +64 -64
- package/src/plugins/form-builder/client/plugin.tsx +19 -18
- package/src/plugins/form-builder/types.ts +19 -24
- package/src/plugins/kanban/api/plugin.ts +111 -136
- package/src/plugins/kanban/client/plugin.tsx +35 -41
- package/src/plugins/ui-builder/client/plugin.tsx +11 -10
- package/src/plugins/ui-builder/types.ts +4 -4
- 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.
|
|
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<
|
|
102
|
+
) => Promise<void> | void;
|
|
102
103
|
/**
|
|
103
|
-
* Called after posts are loaded.
|
|
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<
|
|
113
|
+
) => Promise<void> | void;
|
|
113
114
|
/**
|
|
114
|
-
* Called before loading a single post.
|
|
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<
|
|
122
|
+
) => Promise<void> | void;
|
|
122
123
|
/**
|
|
123
|
-
* Called after a post is loaded.
|
|
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<
|
|
133
|
+
) => Promise<void> | void;
|
|
133
134
|
/**
|
|
134
|
-
* Called before loading the new post page.
|
|
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<
|
|
138
|
+
beforeLoadNewPost?: (context: LoaderContext) => Promise<void> | void;
|
|
138
139
|
/**
|
|
139
|
-
* Called after the new post page is loaded.
|
|
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<
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
206
|
-
posts,
|
|
207
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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 {
|
|
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
|
-
|
|
783
|
-
processedData,
|
|
784
|
-
|
|
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
|
-
|
|
926
|
-
id,
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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.
|
|
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<
|
|
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.
|
|
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<
|
|
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.
|
|
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<
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
325
|
-
typeSlug,
|
|
326
|
-
|
|
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>({
|
package/src/plugins/cms/types.ts
CHANGED
|
@@ -248,16 +248,16 @@ export interface CMSHookContext {
|
|
|
248
248
|
/**
|
|
249
249
|
* Hooks for customizing CMS backend behavior
|
|
250
250
|
*
|
|
251
|
-
* Note: Before hooks
|
|
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.
|
|
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<
|
|
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.
|
|
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<
|
|
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<
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
301
|
-
formInput,
|
|
302
|
-
|
|
304
|
+
const hookResult = await runHookWithShim(
|
|
305
|
+
() => config.hooks!.onBeforeFormCreated!(formInput, context),
|
|
306
|
+
ctx.error,
|
|
307
|
+
"Create operation denied",
|
|
303
308
|
);
|
|
304
|
-
if (
|
|
305
|
-
|
|
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
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
const hookResult = await runHookWithShim(
|
|
416
|
+
() =>
|
|
417
|
+
config.hooks!.onBeforeFormUpdated!(id, updateInput, context),
|
|
418
|
+
ctx.error,
|
|
419
|
+
"Update operation denied",
|
|
417
420
|
);
|
|
418
|
-
if (
|
|
419
|
-
|
|
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
|
-
|
|
489
|
-
id,
|
|
490
|
-
|
|
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
|
-
|
|
583
|
+
hookResult = await config.hooks.onBeforeSubmission(
|
|
582
584
|
slug,
|
|
583
585
|
data as Record<string, unknown>,
|
|
584
586
|
submissionContext,
|
|
585
587
|
);
|
|
586
|
-
|
|
587
|
-
|
|
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 (
|
|
593
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
formId,
|
|
667
|
-
|
|
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
|
-
|
|
691
|
-
subId,
|
|
692
|
-
|
|
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
|
-
|
|
735
|
-
subId,
|
|
736
|
-
|
|
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({
|