@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.
- package/dist/packages/stack/src/client/components/compose.cjs +1 -2
- package/dist/packages/stack/src/client/components/compose.mjs +1 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +87 -54
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +87 -54
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
- 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/ai-chat/schemas.cjs +17 -1
- package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
- 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/components/forms/post-forms.cjs +15 -2
- package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
- package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
- 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/mutations.cjs +48 -0
- package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +21 -18
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +21 -18
- 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/mutations.cjs +91 -0
- package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +92 -118
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +89 -115
- package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
- package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
- 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/components/pages/page-builder-page.internal.cjs +89 -0
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
- 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/components/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
- 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 +10 -10
- package/dist/plugins/ai-chat/client/index.d.mts +10 -10
- package/dist/plugins/ai-chat/client/index.d.ts +10 -10
- 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 +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +13 -13
- package/dist/plugins/blog/client/index.d.mts +13 -13
- package/dist/plugins/blog/client/index.d.ts +13 -13
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- 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.cjs +2 -0
- 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/api/index.mjs +1 -0
- 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.cjs +4 -0
- 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/api/index.mjs +1 -0
- 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.B1EeBt1b.d.ts → stack.B58oHdqm.d.mts} +33 -3
- 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.CIP6QS9l.d.ts → stack.BDVEpue1.d.ts} +1 -1
- package/dist/shared/{stack.C5dtIncc.d.mts → stack.BTvbxZvw.d.cts} +1 -1
- package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
- package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
- package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
- package/dist/shared/{stack.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
- package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.mts} +19 -19
- package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.ts} +19 -19
- package/dist/shared/{stack.CP68pFEH.d.mts → stack.C9Mg2Q46.d.cts} +33 -3
- package/dist/shared/{stack.BeSm90va.d.ts → stack.CTDVxbrA.d.ts} +72 -14
- package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
- package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CxaFNQCV.d.mts} +89 -34
- package/dist/shared/{stack.CMh_EdxW.d.cts → stack.D-b5zbPm.d.cts} +72 -14
- package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.DTtmJPQO.d.mts} +1 -1
- package/dist/shared/{stack.BKfolAyK.d.ts → stack.DXnclTG7.d.ts} +11 -11
- package/dist/shared/{stack.snB1EDP7.d.cts → stack.DaZM10cp.d.cts} +11 -11
- package/dist/shared/{stack.Dg09R0oB.d.mts → stack.FVWf2JhZ.d.mts} +72 -14
- package/dist/shared/{stack.BIXEI6v_.d.mts → stack.cfCkioTe.d.mts} +11 -11
- package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
- package/dist/shared/{stack.BpolpQpf.d.cts → stack.j75TpKh2.d.ts} +89 -34
- package/dist/shared/{stack.rTy7-wQU.d.mts → stack.n1_i1p2B.d.cts} +89 -34
- package/dist/shared/{stack.IdtKDRka.d.cts → stack.sO33ZDhK.d.ts} +33 -3
- package/package.json +14 -1
- package/src/client/components/compose.tsx +7 -4
- package/src/plugins/ai-chat/api/page-tools.ts +111 -0
- package/src/plugins/ai-chat/api/plugin.ts +228 -72
- package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
- package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
- package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
- package/src/plugins/ai-chat/client/plugin.tsx +23 -31
- package/src/plugins/ai-chat/schemas.ts +16 -0
- package/src/plugins/blog/api/plugin.ts +31 -47
- package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
- package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
- package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
- package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
- package/src/plugins/blog/client/plugin.tsx +36 -39
- package/src/plugins/client/index.ts +5 -1
- package/src/plugins/cms/api/index.ts +4 -0
- package/src/plugins/cms/api/mutations.ts +84 -0
- package/src/plugins/cms/api/plugin.ts +23 -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/index.ts +6 -0
- package/src/plugins/kanban/api/mutations.ts +169 -0
- package/src/plugins/kanban/api/plugin.ts +123 -136
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
- package/src/plugins/kanban/client/plugin.tsx +35 -41
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
- 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
- package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.cts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.mts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.ts} +2 -2
|
@@ -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({
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
defineClientPlugin,
|
|
5
5
|
createApiClient,
|
|
6
6
|
isConnectionError,
|
|
7
|
+
runClientHookWithShim,
|
|
7
8
|
} from "@btst/stack/plugins/client";
|
|
8
9
|
import { createRoute } from "@btst/yar";
|
|
9
10
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -53,24 +54,24 @@ export interface LoaderContext {
|
|
|
53
54
|
*/
|
|
54
55
|
export interface FormBuilderClientHooks {
|
|
55
56
|
/**
|
|
56
|
-
* Called before loading the form list page.
|
|
57
|
+
* Called before loading the form list page. Throw an error to cancel loading.
|
|
57
58
|
* @param context - Loader context with path, params, etc.
|
|
58
59
|
*/
|
|
59
|
-
beforeLoadFormList?: (context: LoaderContext) => Promise<
|
|
60
|
+
beforeLoadFormList?: (context: LoaderContext) => Promise<void> | void;
|
|
60
61
|
/**
|
|
61
62
|
* Called after the form list is loaded.
|
|
62
63
|
* @param context - Loader context
|
|
63
64
|
*/
|
|
64
65
|
afterLoadFormList?: (context: LoaderContext) => Promise<void> | void;
|
|
65
66
|
/**
|
|
66
|
-
* Called before loading the form builder page.
|
|
67
|
+
* Called before loading the form builder page. Throw an error to cancel loading.
|
|
67
68
|
* @param id - The form ID (undefined for new forms)
|
|
68
69
|
* @param context - Loader context
|
|
69
70
|
*/
|
|
70
71
|
beforeLoadFormBuilder?: (
|
|
71
72
|
id: string | undefined,
|
|
72
73
|
context: LoaderContext,
|
|
73
|
-
) => Promise<
|
|
74
|
+
) => Promise<void> | void;
|
|
74
75
|
/**
|
|
75
76
|
* Called after the form builder is loaded.
|
|
76
77
|
* @param id - The form ID (undefined for new forms)
|
|
@@ -81,14 +82,14 @@ export interface FormBuilderClientHooks {
|
|
|
81
82
|
context: LoaderContext,
|
|
82
83
|
) => Promise<void> | void;
|
|
83
84
|
/**
|
|
84
|
-
* Called before loading the submissions page.
|
|
85
|
+
* Called before loading the submissions page. Throw an error to cancel loading.
|
|
85
86
|
* @param formId - The form ID
|
|
86
87
|
* @param context - Loader context
|
|
87
88
|
*/
|
|
88
89
|
beforeLoadSubmissions?: (
|
|
89
90
|
formId: string,
|
|
90
91
|
context: LoaderContext,
|
|
91
|
-
) => Promise<
|
|
92
|
+
) => Promise<void> | void;
|
|
92
93
|
/**
|
|
93
94
|
* Called after the submissions page is loaded.
|
|
94
95
|
* @param formId - The form ID
|
|
@@ -146,10 +147,10 @@ function createFormListLoader(config: FormBuilderClientConfig) {
|
|
|
146
147
|
try {
|
|
147
148
|
// Before hook - authorization check
|
|
148
149
|
if (hooks?.beforeLoadFormList) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
await runClientHookWithShim(
|
|
151
|
+
() => hooks.beforeLoadFormList!(context),
|
|
152
|
+
"Load prevented by beforeLoadFormList hook",
|
|
153
|
+
);
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
const client = createApiClient<FormBuilderApiRouter>({
|
|
@@ -235,10 +236,10 @@ function createFormBuilderLoader(
|
|
|
235
236
|
try {
|
|
236
237
|
// Before hook - authorization check
|
|
237
238
|
if (hooks?.beforeLoadFormBuilder) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
await runClientHookWithShim(
|
|
240
|
+
() => hooks.beforeLoadFormBuilder!(id, context),
|
|
241
|
+
"Load prevented by beforeLoadFormBuilder hook",
|
|
242
|
+
);
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
const client = createApiClient<FormBuilderApiRouter>({
|
|
@@ -309,10 +310,10 @@ function createSubmissionsLoader(
|
|
|
309
310
|
try {
|
|
310
311
|
// Before hook - authorization check
|
|
311
312
|
if (hooks?.beforeLoadSubmissions) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
await runClientHookWithShim(
|
|
314
|
+
() => hooks.beforeLoadSubmissions!(formId, context),
|
|
315
|
+
"Load prevented by beforeLoadSubmissions hook",
|
|
316
|
+
);
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
const client = createApiClient<FormBuilderApiRouter>({
|
|
@@ -167,23 +167,21 @@ export interface FormUpdate {
|
|
|
167
167
|
* Backend hooks for Form Builder plugin
|
|
168
168
|
*
|
|
169
169
|
* All CRUD hooks receive ipAddress and headers for auth/rate limiting.
|
|
170
|
-
*
|
|
170
|
+
* Throw an error from onBefore* hooks to reject the operation (throws 403).
|
|
171
171
|
*/
|
|
172
172
|
export interface FormBuilderBackendHooks {
|
|
173
173
|
// ============================================================================
|
|
174
174
|
// FORM CRUD HOOKS (Admin operations)
|
|
175
175
|
// ============================================================================
|
|
176
176
|
|
|
177
|
-
/** Called before listing forms.
|
|
178
|
-
onBeforeListForms?: (
|
|
179
|
-
ctx: FormBuilderHookContext,
|
|
180
|
-
) => Promise<boolean> | boolean;
|
|
177
|
+
/** Called before listing forms. Throw an error to deny access (403). */
|
|
178
|
+
onBeforeListForms?: (ctx: FormBuilderHookContext) => Promise<void> | void;
|
|
181
179
|
|
|
182
|
-
/** Called before creating a form.
|
|
180
|
+
/** Called before creating a form. Throw an error to deny, or return modified data. */
|
|
183
181
|
onBeforeFormCreated?: (
|
|
184
182
|
data: FormInput,
|
|
185
183
|
ctx: FormBuilderHookContext,
|
|
186
|
-
) => Promise<FormInput |
|
|
184
|
+
) => Promise<FormInput | void> | FormInput | void;
|
|
187
185
|
|
|
188
186
|
/** Called after a form is created */
|
|
189
187
|
onAfterFormCreated?: (
|
|
@@ -191,18 +189,18 @@ export interface FormBuilderBackendHooks {
|
|
|
191
189
|
ctx: FormBuilderHookContext,
|
|
192
190
|
) => Promise<void> | void;
|
|
193
191
|
|
|
194
|
-
/** Called before getting a form by ID or slug.
|
|
192
|
+
/** Called before getting a form by ID or slug. Throw an error to deny access. */
|
|
195
193
|
onBeforeGetForm?: (
|
|
196
194
|
idOrSlug: string,
|
|
197
195
|
ctx: FormBuilderHookContext,
|
|
198
|
-
) => Promise<
|
|
196
|
+
) => Promise<void> | void;
|
|
199
197
|
|
|
200
|
-
/** Called before updating a form.
|
|
198
|
+
/** Called before updating a form. Throw an error to deny, or return modified data. */
|
|
201
199
|
onBeforeFormUpdated?: (
|
|
202
200
|
id: string,
|
|
203
201
|
data: FormUpdate,
|
|
204
202
|
ctx: FormBuilderHookContext,
|
|
205
|
-
) => Promise<FormUpdate |
|
|
203
|
+
) => Promise<FormUpdate | void> | FormUpdate | void;
|
|
206
204
|
|
|
207
205
|
/** Called after a form is updated */
|
|
208
206
|
onAfterFormUpdated?: (
|
|
@@ -210,11 +208,11 @@ export interface FormBuilderBackendHooks {
|
|
|
210
208
|
ctx: FormBuilderHookContext,
|
|
211
209
|
) => Promise<void> | void;
|
|
212
210
|
|
|
213
|
-
/** Called before deleting a form.
|
|
211
|
+
/** Called before deleting a form. Throw an error to deny. */
|
|
214
212
|
onBeforeFormDeleted?: (
|
|
215
213
|
id: string,
|
|
216
214
|
ctx: FormBuilderHookContext,
|
|
217
|
-
) => Promise<
|
|
215
|
+
) => Promise<void> | void;
|
|
218
216
|
|
|
219
217
|
/** Called after a form is deleted */
|
|
220
218
|
onAfterFormDeleted?: (
|
|
@@ -230,16 +228,13 @@ export interface FormBuilderBackendHooks {
|
|
|
230
228
|
* Called before processing a form submission.
|
|
231
229
|
* Use for: spam protection, rate limiting, data validation/enrichment.
|
|
232
230
|
*
|
|
233
|
-
*
|
|
231
|
+
* Throw an error to reject submission (400), or return modified data to continue.
|
|
234
232
|
*/
|
|
235
233
|
onBeforeSubmission?: (
|
|
236
234
|
formSlug: string,
|
|
237
235
|
data: Record<string, unknown>,
|
|
238
236
|
ctx: SubmissionHookContext,
|
|
239
|
-
) =>
|
|
240
|
-
| Promise<Record<string, unknown> | false>
|
|
241
|
-
| Record<string, unknown>
|
|
242
|
-
| false;
|
|
237
|
+
) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void;
|
|
243
238
|
|
|
244
239
|
/**
|
|
245
240
|
* Called after a submission is saved.
|
|
@@ -263,23 +258,23 @@ export interface FormBuilderBackendHooks {
|
|
|
263
258
|
// SUBMISSIONS MANAGEMENT HOOKS (Admin viewing submissions)
|
|
264
259
|
// ============================================================================
|
|
265
260
|
|
|
266
|
-
/** Called before listing submissions.
|
|
261
|
+
/** Called before listing submissions. Throw an error to deny access (403). */
|
|
267
262
|
onBeforeListSubmissions?: (
|
|
268
263
|
formId: string,
|
|
269
264
|
ctx: FormBuilderHookContext,
|
|
270
|
-
) => Promise<
|
|
265
|
+
) => Promise<void> | void;
|
|
271
266
|
|
|
272
|
-
/** Called before getting a submission.
|
|
267
|
+
/** Called before getting a submission. Throw an error to deny access. */
|
|
273
268
|
onBeforeGetSubmission?: (
|
|
274
269
|
submissionId: string,
|
|
275
270
|
ctx: FormBuilderHookContext,
|
|
276
|
-
) => Promise<
|
|
271
|
+
) => Promise<void> | void;
|
|
277
272
|
|
|
278
|
-
/** Called before deleting a submission.
|
|
273
|
+
/** Called before deleting a submission. Throw an error to deny. */
|
|
279
274
|
onBeforeSubmissionDeleted?: (
|
|
280
275
|
submissionId: string,
|
|
281
276
|
ctx: FormBuilderHookContext,
|
|
282
|
-
) => Promise<
|
|
277
|
+
) => Promise<void> | void;
|
|
283
278
|
|
|
284
279
|
/** Called after a submission is deleted */
|
|
285
280
|
onAfterSubmissionDeleted?: (
|
|
@@ -6,5 +6,11 @@ export {
|
|
|
6
6
|
type KanbanBackendHooks,
|
|
7
7
|
} from "./plugin";
|
|
8
8
|
export { getAllBoards, getBoardById, type BoardListResult } from "./getters";
|
|
9
|
+
export {
|
|
10
|
+
createKanbanTask,
|
|
11
|
+
findOrCreateKanbanBoard,
|
|
12
|
+
getKanbanColumnsByBoardId,
|
|
13
|
+
type CreateKanbanTaskInput,
|
|
14
|
+
} from "./mutations";
|
|
9
15
|
export { serializeBoard, serializeColumn, serializeTask } from "./serializers";
|
|
10
16
|
export { KANBAN_QUERY_KEYS } from "./query-key-defs";
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import type { Board, Column, Task, Priority } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Input for creating a new Kanban task.
|
|
6
|
+
*/
|
|
7
|
+
export interface CreateKanbanTaskInput {
|
|
8
|
+
title: string;
|
|
9
|
+
columnId: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
priority?: Priority;
|
|
12
|
+
assigneeId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a new task in a Kanban column.
|
|
17
|
+
* Computes the next order value from existing tasks in the column.
|
|
18
|
+
*
|
|
19
|
+
* @remarks **Security:** No authorization hooks (onBeforeCreateTask) are called.
|
|
20
|
+
* The caller is responsible for any access-control checks before invoking this
|
|
21
|
+
* function.
|
|
22
|
+
*
|
|
23
|
+
* @param adapter - The database adapter
|
|
24
|
+
* @param input - Task creation input
|
|
25
|
+
*/
|
|
26
|
+
export async function createKanbanTask(
|
|
27
|
+
adapter: Adapter,
|
|
28
|
+
input: CreateKanbanTaskInput,
|
|
29
|
+
): Promise<Task> {
|
|
30
|
+
const existingTasks = await adapter.findMany<Task>({
|
|
31
|
+
model: "kanbanTask",
|
|
32
|
+
where: [
|
|
33
|
+
{
|
|
34
|
+
field: "columnId",
|
|
35
|
+
value: input.columnId,
|
|
36
|
+
operator: "eq" as const,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const nextOrder =
|
|
42
|
+
existingTasks.length > 0
|
|
43
|
+
? Math.max(...existingTasks.map((t) => t.order)) + 1
|
|
44
|
+
: 0;
|
|
45
|
+
|
|
46
|
+
return adapter.create<Task>({
|
|
47
|
+
model: "kanbanTask",
|
|
48
|
+
data: {
|
|
49
|
+
title: input.title,
|
|
50
|
+
columnId: input.columnId,
|
|
51
|
+
description: input.description,
|
|
52
|
+
priority: input.priority ?? "MEDIUM",
|
|
53
|
+
order: nextOrder,
|
|
54
|
+
assigneeId: input.assigneeId,
|
|
55
|
+
isArchived: false,
|
|
56
|
+
createdAt: new Date(),
|
|
57
|
+
updatedAt: new Date(),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Coalesces concurrent `findOrCreateKanbanBoard` calls within the same process.
|
|
64
|
+
* Keyed by slug; entries are removed once the creation promise settles.
|
|
65
|
+
*/
|
|
66
|
+
const _pendingBoardCreations = new Map<string, Promise<Board>>();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Find a board by slug, or create it with the given name and custom column titles.
|
|
70
|
+
*
|
|
71
|
+
* Concurrency-safe at two levels:
|
|
72
|
+
* - **Same process**: concurrent calls with the same slug share a single in-flight
|
|
73
|
+
* Promise (via `_pendingBoardCreations`), so only one DB write is attempted.
|
|
74
|
+
* - **Cross-instance**: the DB `unique` constraint on `slug` causes the losing
|
|
75
|
+
* write to throw; the catch block re-fetches and returns the winner's board.
|
|
76
|
+
*
|
|
77
|
+
* @remarks **Security:** No authorization hooks are called. The caller is
|
|
78
|
+
* responsible for any access-control checks before invoking this function.
|
|
79
|
+
*
|
|
80
|
+
* @param adapter - The database adapter
|
|
81
|
+
* @param slug - Unique URL-safe slug for the board
|
|
82
|
+
* @param name - Display name for the board (used only on creation)
|
|
83
|
+
* @param columnTitles - Ordered list of column names to create (used only on creation)
|
|
84
|
+
*/
|
|
85
|
+
export async function findOrCreateKanbanBoard(
|
|
86
|
+
adapter: Adapter,
|
|
87
|
+
slug: string,
|
|
88
|
+
name: string,
|
|
89
|
+
columnTitles: string[],
|
|
90
|
+
): Promise<Board> {
|
|
91
|
+
const existing = await adapter.findOne<Board>({
|
|
92
|
+
model: "kanbanBoard",
|
|
93
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (existing) return existing;
|
|
97
|
+
|
|
98
|
+
// Coalesce same-process concurrent calls for this slug
|
|
99
|
+
const inflight = _pendingBoardCreations.get(slug);
|
|
100
|
+
if (inflight) return inflight;
|
|
101
|
+
|
|
102
|
+
const creation = (async () => {
|
|
103
|
+
try {
|
|
104
|
+
const board = await adapter.create<Board>({
|
|
105
|
+
model: "kanbanBoard",
|
|
106
|
+
data: {
|
|
107
|
+
name,
|
|
108
|
+
slug,
|
|
109
|
+
createdAt: new Date(),
|
|
110
|
+
updatedAt: new Date(),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await Promise.all(
|
|
115
|
+
columnTitles.map((title, index) =>
|
|
116
|
+
adapter.create<Column>({
|
|
117
|
+
model: "kanbanColumn",
|
|
118
|
+
data: {
|
|
119
|
+
title,
|
|
120
|
+
boardId: board.id,
|
|
121
|
+
order: index,
|
|
122
|
+
createdAt: new Date(),
|
|
123
|
+
updatedAt: new Date(),
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return board;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Cross-instance race: another process won the unique-constraint race.
|
|
132
|
+
// Re-fetch so all callers return the same board.
|
|
133
|
+
const winner = await adapter.findOne<Board>({
|
|
134
|
+
model: "kanbanBoard",
|
|
135
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
136
|
+
});
|
|
137
|
+
if (winner) return winner;
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
|
|
142
|
+
_pendingBoardCreations.set(slug, creation);
|
|
143
|
+
try {
|
|
144
|
+
return await creation;
|
|
145
|
+
} finally {
|
|
146
|
+
_pendingBoardCreations.delete(slug);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Retrieve all columns for a given board, sorted by order.
|
|
152
|
+
* Co-located with mutations because it is primarily used alongside
|
|
153
|
+
* {@link createKanbanTask} to resolve column IDs before task creation.
|
|
154
|
+
*
|
|
155
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
156
|
+
*
|
|
157
|
+
* @param adapter - The database adapter
|
|
158
|
+
* @param boardId - The board ID
|
|
159
|
+
*/
|
|
160
|
+
export async function getKanbanColumnsByBoardId(
|
|
161
|
+
adapter: Adapter,
|
|
162
|
+
boardId: string,
|
|
163
|
+
): Promise<Column[]> {
|
|
164
|
+
return adapter.findMany<Column>({
|
|
165
|
+
model: "kanbanColumn",
|
|
166
|
+
where: [{ field: "boardId", value: boardId, operator: "eq" as const }],
|
|
167
|
+
sortBy: { field: "order", direction: "asc" },
|
|
168
|
+
});
|
|
169
|
+
}
|