@btst/stack 2.2.0 → 2.4.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 +54 -7
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +54 -7
- 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/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 +52 -1
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +52 -1
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
- 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 +15 -0
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/cms/api/getters.cjs +10 -0
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +10 -1
- 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 +75 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +76 -1
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
- package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +9 -0
- package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +9 -1
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +62 -1
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +63 -2
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -1
- 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 +34 -1
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +34 -1
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
- 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 +10 -0
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +11 -1
- 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/utils.cjs +6 -0
- package/dist/packages/stack/src/plugins/utils.mjs +6 -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 +2 -2
- package/dist/plugins/ai-chat/client/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/index.d.ts +2 -2
- 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.cjs +5 -0
- package/dist/plugins/blog/api/index.d.cts +19 -4
- package/dist/plugins/blog/api/index.d.mts +19 -4
- package/dist/plugins/blog/api/index.d.ts +19 -4
- package/dist/plugins/blog/api/index.mjs +2 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.mts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.ts +3 -3
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.cjs +6 -5
- package/dist/plugins/blog/query-keys.d.cts +8 -387
- package/dist/plugins/blog/query-keys.d.mts +8 -387
- package/dist/plugins/blog/query-keys.d.ts +8 -387
- package/dist/plugins/blog/query-keys.mjs +6 -5
- 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 +8 -0
- package/dist/plugins/cms/api/index.d.cts +7 -219
- package/dist/plugins/cms/api/index.d.mts +7 -219
- package/dist/plugins/cms/api/index.d.ts +7 -219
- package/dist/plugins/cms/api/index.mjs +3 -1
- 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/query-keys.cjs +2 -1
- package/dist/plugins/cms/query-keys.d.cts +5 -9
- package/dist/plugins/cms/query-keys.d.mts +5 -9
- package/dist/plugins/cms/query-keys.d.ts +5 -9
- package/dist/plugins/cms/query-keys.mjs +2 -1
- package/dist/plugins/form-builder/api/index.cjs +6 -0
- package/dist/plugins/form-builder/api/index.d.cts +7 -211
- package/dist/plugins/form-builder/api/index.d.mts +7 -211
- package/dist/plugins/form-builder/api/index.d.ts +7 -211
- package/dist/plugins/form-builder/api/index.mjs +2 -1
- 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/query-keys.cjs +3 -2
- package/dist/plugins/form-builder/query-keys.d.cts +6 -6
- package/dist/plugins/form-builder/query-keys.d.mts +6 -6
- package/dist/plugins/form-builder/query-keys.d.ts +6 -6
- package/dist/plugins/form-builder/query-keys.mjs +3 -2
- package/dist/plugins/kanban/api/index.cjs +10 -0
- package/dist/plugins/kanban/api/index.d.cts +17 -392
- package/dist/plugins/kanban/api/index.d.mts +17 -392
- package/dist/plugins/kanban/api/index.d.ts +17 -392
- package/dist/plugins/kanban/api/index.mjs +3 -0
- package/dist/plugins/kanban/client/components/index.d.cts +1 -1
- package/dist/plugins/kanban/client/components/index.d.mts +1 -1
- package/dist/plugins/kanban/client/components/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.cjs +2 -9
- package/dist/plugins/kanban/query-keys.d.cts +4 -16
- package/dist/plugins/kanban/query-keys.d.mts +4 -16
- package/dist/plugins/kanban/query-keys.d.ts +4 -16
- package/dist/plugins/kanban/query-keys.mjs +2 -9
- package/dist/plugins/ui-builder/index.d.cts +1 -1
- package/dist/plugins/ui-builder/index.d.mts +1 -1
- package/dist/plugins/ui-builder/index.d.ts +1 -1
- package/dist/shared/stack.B7ONvlD_.d.mts +293 -0
- package/dist/shared/{stack.BeSm90va.d.ts → stack.BEn34wW6.d.ts} +60 -2
- package/dist/shared/stack.BUkC2EsZ.d.cts +327 -0
- 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.BepFXT3w.d.mts +500 -0
- package/dist/shared/stack.CL8ts1Mu.d.ts +419 -0
- package/dist/shared/{stack.CXjzTMsb.d.cts → stack.CVDTkMoO.d.cts} +7 -1
- package/dist/shared/{stack.CXjzTMsb.d.mts → stack.CVDTkMoO.d.mts} +7 -1
- package/dist/shared/{stack.CXjzTMsb.d.ts → stack.CVDTkMoO.d.ts} +7 -1
- package/dist/shared/stack.CczspVn2.d.mts +327 -0
- package/dist/shared/stack.CgWzG5jH.d.ts +500 -0
- package/dist/shared/stack.D3GB6wKv.d.cts +500 -0
- package/dist/shared/stack.DASmUVjX.d.ts +327 -0
- package/dist/shared/{stack.QD1y_7NY.d.cts → stack.DJaKVY7v.d.cts} +1 -1
- package/dist/shared/{stack.QD1y_7NY.d.mts → stack.DJaKVY7v.d.mts} +1 -1
- package/dist/shared/{stack.QD1y_7NY.d.ts → stack.DJaKVY7v.d.ts} +1 -1
- package/dist/shared/{stack.Dg09R0oB.d.mts → stack.DTDxgFj8.d.mts} +60 -2
- package/dist/shared/{stack.CMh_EdxW.d.cts → stack.DWoCZff7.d.cts} +60 -2
- package/dist/shared/{stack.CIrIsc-A.d.cts → stack.DdI5W6MB.d.cts} +7 -1
- package/dist/shared/{stack.CIrIsc-A.d.mts → stack.DdI5W6MB.d.mts} +7 -1
- package/dist/shared/{stack.CIrIsc-A.d.ts → stack.DdI5W6MB.d.ts} +7 -1
- package/dist/shared/stack.Dk5r4W1F.d.mts +419 -0
- package/dist/shared/stack.Kq2-QzOC.d.ts +293 -0
- package/dist/shared/stack.heOA9gzA.d.cts +419 -0
- package/dist/shared/stack.kcdnD4gA.d.cts +293 -0
- package/package.json +16 -3
- 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 +180 -9
- 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/schemas.ts +16 -0
- package/src/plugins/blog/api/index.ts +2 -0
- package/src/plugins/blog/api/plugin.ts +85 -0
- package/src/plugins/blog/api/query-key-defs.ts +46 -0
- package/src/plugins/blog/api/serializers.ts +27 -0
- 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 +19 -0
- package/src/plugins/blog/query-keys.ts +5 -7
- package/src/plugins/client/index.ts +1 -1
- package/src/plugins/cms/api/getters.ts +24 -0
- package/src/plugins/cms/api/index.ts +14 -1
- package/src/plugins/cms/api/mutations.ts +84 -0
- package/src/plugins/cms/api/plugin.ts +114 -0
- package/src/plugins/cms/api/query-key-defs.ts +53 -0
- package/src/plugins/cms/api/serializers.ts +12 -0
- package/src/plugins/cms/client/plugin.tsx +19 -0
- package/src/plugins/cms/query-keys.ts +2 -1
- package/src/plugins/form-builder/api/getters.ts +23 -0
- package/src/plugins/form-builder/api/index.ts +15 -2
- package/src/plugins/form-builder/api/plugin.ts +91 -0
- package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
- package/src/plugins/form-builder/api/serializers.ts +12 -0
- package/src/plugins/form-builder/client/plugin.tsx +19 -0
- package/src/plugins/form-builder/query-keys.ts +6 -2
- package/src/plugins/kanban/api/index.ts +9 -0
- package/src/plugins/kanban/api/mutations.ts +169 -0
- package/src/plugins/kanban/api/plugin.ts +61 -0
- package/src/plugins/kanban/api/query-key-defs.ts +54 -0
- package/src/plugins/kanban/api/serializers.ts +49 -0
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
- package/src/plugins/kanban/client/plugin.tsx +13 -0
- package/src/plugins/kanban/query-keys.ts +2 -9
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
- package/src/plugins/utils.ts +19 -0
- package/dist/shared/{stack.BkYlUT_8.d.cts → stack.BQmuNl5p.d.cts} +6 -6
- package/dist/shared/{stack.BkYlUT_8.d.mts → stack.BQmuNl5p.d.mts} +6 -6
- package/dist/shared/{stack.BkYlUT_8.d.ts → stack.BQmuNl5p.d.ts} +6 -6
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal query key constants for the CMS plugin.
|
|
3
|
+
* Shared between query-keys.ts (HTTP path) and prefetchForRoute (DB path)
|
|
4
|
+
* to prevent key drift between SSR loaders and SSG prefetching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ContentListDiscriminator {
|
|
8
|
+
typeSlug: string;
|
|
9
|
+
limit: number;
|
|
10
|
+
offset: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds the discriminator object used as the cache key for the content list.
|
|
15
|
+
* Mirrors the params object used in createContentQueries.list so both paths stay in sync.
|
|
16
|
+
*/
|
|
17
|
+
export function contentListDiscriminator(params: {
|
|
18
|
+
typeSlug: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
offset?: number;
|
|
21
|
+
}): ContentListDiscriminator {
|
|
22
|
+
return {
|
|
23
|
+
typeSlug: params.typeSlug,
|
|
24
|
+
limit: params.limit ?? 20,
|
|
25
|
+
offset: params.offset ?? 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Full query key builders — use these with queryClient.setQueryData() */
|
|
30
|
+
export const CMS_QUERY_KEYS = {
|
|
31
|
+
/**
|
|
32
|
+
* Key for the cmsTypes.list() query.
|
|
33
|
+
* Full key: ["cmsTypes", "list", "list"]
|
|
34
|
+
*/
|
|
35
|
+
typesList: () => ["cmsTypes", "list", "list"] as const,
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Key for the cmsContent.list({ typeSlug, limit, offset }) query.
|
|
39
|
+
* Full key: ["cmsContent", "list", { typeSlug, limit, offset }]
|
|
40
|
+
*/
|
|
41
|
+
contentList: (params: {
|
|
42
|
+
typeSlug: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
offset?: number;
|
|
45
|
+
}) => ["cmsContent", "list", contentListDiscriminator(params)] as const,
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Key for the cmsContent.detail(typeSlug, id) query.
|
|
49
|
+
* Full key: ["cmsContent", "detail", typeSlug, id]
|
|
50
|
+
*/
|
|
51
|
+
contentDetail: (typeSlug: string, id: string) =>
|
|
52
|
+
["cmsContent", "detail", typeSlug, id] as const,
|
|
53
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports serialization helpers from getters.ts for consumers who import
|
|
3
|
+
* from @btst/stack/plugins/cms/api.
|
|
4
|
+
*
|
|
5
|
+
* The actual implementations live in getters.ts alongside the DB functions
|
|
6
|
+
* they serialize so they stay in sync with the returned types.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
serializeContentType,
|
|
10
|
+
serializeContentItem,
|
|
11
|
+
serializeContentItemWithType,
|
|
12
|
+
} from "./getters";
|
|
@@ -2,6 +2,7 @@ import { lazy } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
defineClientPlugin,
|
|
4
4
|
createApiClient,
|
|
5
|
+
isConnectionError,
|
|
5
6
|
} from "@btst/stack/plugins/client";
|
|
6
7
|
import { createRoute } from "@btst/yar";
|
|
7
8
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -181,6 +182,12 @@ function createDashboardLoader(config: CMSClientConfig) {
|
|
|
181
182
|
} catch (error) {
|
|
182
183
|
// Error hook - log the error but don't throw during SSR
|
|
183
184
|
// Let Error Boundaries handle errors when components render
|
|
185
|
+
if (isConnectionError(error)) {
|
|
186
|
+
console.warn(
|
|
187
|
+
"[btst/cms] route.loader() failed — no server running at build time. " +
|
|
188
|
+
"Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
184
191
|
if (hooks?.onLoadError) {
|
|
185
192
|
await hooks.onLoadError(error as Error, context);
|
|
186
193
|
}
|
|
@@ -275,6 +282,12 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
|
|
|
275
282
|
} catch (error) {
|
|
276
283
|
// Error hook - log the error but don't throw during SSR
|
|
277
284
|
// Let Error Boundaries handle errors when components render
|
|
285
|
+
if (isConnectionError(error)) {
|
|
286
|
+
console.warn(
|
|
287
|
+
"[btst/cms] route.loader() failed — no server running at build time. " +
|
|
288
|
+
"Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
278
291
|
if (hooks?.onLoadError) {
|
|
279
292
|
await hooks.onLoadError(error as Error, context);
|
|
280
293
|
}
|
|
@@ -357,6 +370,12 @@ function createContentEditorLoader(
|
|
|
357
370
|
} catch (error) {
|
|
358
371
|
// Error hook - log the error but don't throw during SSR
|
|
359
372
|
// Let Error Boundaries handle errors when components render
|
|
373
|
+
if (isConnectionError(error)) {
|
|
374
|
+
console.warn(
|
|
375
|
+
"[btst/cms] route.loader() failed — no server running at build time. " +
|
|
376
|
+
"Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
360
379
|
if (hooks?.onLoadError) {
|
|
361
380
|
await hooks.onLoadError(error as Error, context);
|
|
362
381
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
SerializedContentItemWithType,
|
|
10
10
|
PaginatedContentItems,
|
|
11
11
|
} from "./types";
|
|
12
|
+
import { contentListDiscriminator } from "./api/query-key-defs";
|
|
12
13
|
|
|
13
14
|
interface ContentListParams {
|
|
14
15
|
limit?: number;
|
|
@@ -115,7 +116,7 @@ function createContentQueries(
|
|
|
115
116
|
) {
|
|
116
117
|
return createQueryKeys("cmsContent", {
|
|
117
118
|
list: (params: { typeSlug: string } & ContentListParams) => ({
|
|
118
|
-
queryKey: [params],
|
|
119
|
+
queryKey: [contentListDiscriminator(params)],
|
|
119
120
|
queryFn: async () => {
|
|
120
121
|
try {
|
|
121
122
|
const response: unknown = await client("/content/:typeSlug", {
|
|
@@ -116,6 +116,29 @@ export async function getAllForms(
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Retrieve a single form by its ID (UUID).
|
|
121
|
+
* Returns null if the form is not found.
|
|
122
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
123
|
+
*
|
|
124
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
125
|
+
* responsible for any access-control checks before invoking this function.
|
|
126
|
+
*
|
|
127
|
+
* @param adapter - The database adapter
|
|
128
|
+
* @param id - The form UUID
|
|
129
|
+
*/
|
|
130
|
+
export async function getFormById(
|
|
131
|
+
adapter: Adapter,
|
|
132
|
+
id: string,
|
|
133
|
+
): Promise<SerializedForm | null> {
|
|
134
|
+
const form = await adapter.findOne<Form>({
|
|
135
|
+
model: "form",
|
|
136
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
137
|
+
});
|
|
138
|
+
if (!form) return null;
|
|
139
|
+
return serializeForm(form);
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
/**
|
|
120
143
|
* Retrieve a single form by its slug.
|
|
121
144
|
* Returns null if the form is not found.
|
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
formBuilderBackendPlugin,
|
|
3
|
+
type FormBuilderApiRouter,
|
|
4
|
+
type FormBuilderRouteKey,
|
|
5
|
+
} from "./plugin";
|
|
6
|
+
export {
|
|
7
|
+
getAllForms,
|
|
8
|
+
getFormById,
|
|
9
|
+
getFormBySlug,
|
|
10
|
+
getFormSubmissions,
|
|
11
|
+
serializeForm,
|
|
12
|
+
serializeFormSubmission,
|
|
13
|
+
serializeFormSubmissionWithData,
|
|
14
|
+
} from "./getters";
|
|
15
|
+
export { FORM_QUERY_KEYS } from "./query-key-defs";
|
|
@@ -23,12 +23,101 @@ import {
|
|
|
23
23
|
import { slugify, extractIpAddress, extractUserAgent } from "../utils";
|
|
24
24
|
import {
|
|
25
25
|
getAllForms,
|
|
26
|
+
getFormById as getFormByIdFromDb,
|
|
26
27
|
getFormBySlug as getFormBySlugFromDb,
|
|
27
28
|
getFormSubmissions,
|
|
28
29
|
serializeForm,
|
|
29
30
|
serializeFormSubmission,
|
|
30
31
|
serializeFormSubmissionWithData,
|
|
31
32
|
} from "./getters";
|
|
33
|
+
import { FORM_QUERY_KEYS } from "./query-key-defs";
|
|
34
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Route keys for the Form Builder plugin — matches the keys returned by
|
|
38
|
+
* `stackClient.router.getRoute(path).routeKey`.
|
|
39
|
+
*/
|
|
40
|
+
export type FormBuilderRouteKey =
|
|
41
|
+
| "formList"
|
|
42
|
+
| "newForm"
|
|
43
|
+
| "editForm"
|
|
44
|
+
| "submissions";
|
|
45
|
+
|
|
46
|
+
interface FormBuilderPrefetchForRoute {
|
|
47
|
+
(key: "formList" | "newForm", qc: QueryClient): Promise<void>;
|
|
48
|
+
(
|
|
49
|
+
key: "editForm" | "submissions",
|
|
50
|
+
qc: QueryClient,
|
|
51
|
+
params: { id: string },
|
|
52
|
+
): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createFormBuilderPrefetchForRoute(
|
|
56
|
+
adapter: Parameters<typeof getAllForms>[0],
|
|
57
|
+
): FormBuilderPrefetchForRoute {
|
|
58
|
+
return async function prefetchForRoute(
|
|
59
|
+
key: FormBuilderRouteKey,
|
|
60
|
+
qc: QueryClient,
|
|
61
|
+
params?: Record<string, string>,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
switch (key) {
|
|
64
|
+
case "formList": {
|
|
65
|
+
const result = await getAllForms(adapter, { limit: 20, offset: 0 });
|
|
66
|
+
qc.setQueryData(FORM_QUERY_KEYS.formsList({ limit: 20, offset: 0 }), {
|
|
67
|
+
pages: [
|
|
68
|
+
{
|
|
69
|
+
items: result.items,
|
|
70
|
+
total: result.total,
|
|
71
|
+
limit: result.limit ?? 20,
|
|
72
|
+
offset: result.offset ?? 0,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
pageParams: [0],
|
|
76
|
+
});
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "editForm": {
|
|
80
|
+
const id = params?.id ?? "";
|
|
81
|
+
if (id) {
|
|
82
|
+
const form = await getFormByIdFromDb(adapter, id);
|
|
83
|
+
qc.setQueryData(FORM_QUERY_KEYS.formById(id), form);
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "submissions": {
|
|
88
|
+
const id = params?.id ?? "";
|
|
89
|
+
if (id) {
|
|
90
|
+
const [form, submissionsResult] = await Promise.all([
|
|
91
|
+
getFormByIdFromDb(adapter, id),
|
|
92
|
+
getFormSubmissions(adapter, id, { limit: 20, offset: 0 }),
|
|
93
|
+
]);
|
|
94
|
+
qc.setQueryData(FORM_QUERY_KEYS.formById(id), form);
|
|
95
|
+
qc.setQueryData(
|
|
96
|
+
FORM_QUERY_KEYS.submissionsList({
|
|
97
|
+
formId: id,
|
|
98
|
+
limit: 20,
|
|
99
|
+
offset: 0,
|
|
100
|
+
}),
|
|
101
|
+
{
|
|
102
|
+
pages: [
|
|
103
|
+
{
|
|
104
|
+
items: submissionsResult.items,
|
|
105
|
+
total: submissionsResult.total,
|
|
106
|
+
limit: submissionsResult.limit ?? 20,
|
|
107
|
+
offset: submissionsResult.offset ?? 0,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
pageParams: [0],
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
default:
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
} as FormBuilderPrefetchForRoute;
|
|
120
|
+
}
|
|
32
121
|
|
|
33
122
|
/**
|
|
34
123
|
* Form Builder backend plugin
|
|
@@ -47,11 +136,13 @@ export const formBuilderBackendPlugin = (
|
|
|
47
136
|
api: (adapter) => ({
|
|
48
137
|
getAllForms: (params?: Parameters<typeof getAllForms>[1]) =>
|
|
49
138
|
getAllForms(adapter, params),
|
|
139
|
+
getFormById: (id: string) => getFormByIdFromDb(adapter, id),
|
|
50
140
|
getFormBySlug: (slug: string) => getFormBySlugFromDb(adapter, slug),
|
|
51
141
|
getFormSubmissions: (
|
|
52
142
|
formId: string,
|
|
53
143
|
params?: Parameters<typeof getFormSubmissions>[2],
|
|
54
144
|
) => getFormSubmissions(adapter, formId, params),
|
|
145
|
+
prefetchForRoute: createFormBuilderPrefetchForRoute(adapter),
|
|
55
146
|
}),
|
|
56
147
|
|
|
57
148
|
routes: (adapter: Adapter) => {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal query key constants for the Form Builder plugin.
|
|
3
|
+
* Shared between query-keys.ts (HTTP path) and prefetchForRoute (DB path)
|
|
4
|
+
* to prevent key drift between SSR loaders and SSG prefetching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface FormsListDiscriminator {
|
|
8
|
+
status?: "active" | "inactive" | "archived";
|
|
9
|
+
limit: number;
|
|
10
|
+
offset: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SubmissionsListDiscriminator {
|
|
14
|
+
formId: string;
|
|
15
|
+
limit: number;
|
|
16
|
+
offset: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds the discriminator object for the forms list query key.
|
|
21
|
+
* Mirrors the params object used in createFormsQueries.list.
|
|
22
|
+
*/
|
|
23
|
+
export function formsListDiscriminator(params?: {
|
|
24
|
+
status?: "active" | "inactive" | "archived";
|
|
25
|
+
limit?: number;
|
|
26
|
+
offset?: number;
|
|
27
|
+
}): FormsListDiscriminator {
|
|
28
|
+
return {
|
|
29
|
+
status: params?.status,
|
|
30
|
+
limit: params?.limit ?? 20,
|
|
31
|
+
offset: params?.offset ?? 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the discriminator object for the submissions list query key.
|
|
37
|
+
* Mirrors the params object used in createSubmissionsQueries.list.
|
|
38
|
+
*/
|
|
39
|
+
export function submissionsListDiscriminator(params: {
|
|
40
|
+
formId: string;
|
|
41
|
+
limit?: number;
|
|
42
|
+
offset?: number;
|
|
43
|
+
}): SubmissionsListDiscriminator {
|
|
44
|
+
return {
|
|
45
|
+
formId: params.formId,
|
|
46
|
+
limit: params.limit ?? 20,
|
|
47
|
+
offset: params.offset ?? 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Full query key builders — use these with queryClient.setQueryData() */
|
|
52
|
+
export const FORM_QUERY_KEYS = {
|
|
53
|
+
/**
|
|
54
|
+
* Key for forms.list(params) query.
|
|
55
|
+
* Full key: ["forms", "list", "list", { status, limit, offset }]
|
|
56
|
+
*/
|
|
57
|
+
formsList: (params?: {
|
|
58
|
+
status?: "active" | "inactive" | "archived";
|
|
59
|
+
limit?: number;
|
|
60
|
+
offset?: number;
|
|
61
|
+
}) => ["forms", "list", "list", formsListDiscriminator(params)] as const,
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Key for forms.byId(id) query.
|
|
65
|
+
* Full key: ["forms", "byId", "byId", id]
|
|
66
|
+
*/
|
|
67
|
+
formById: (id: string) => ["forms", "byId", "byId", id] as const,
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Key for formSubmissions.list(params) query.
|
|
71
|
+
* Full key: ["formSubmissions", "list", { formId, limit, offset }]
|
|
72
|
+
*/
|
|
73
|
+
submissionsList: (params: {
|
|
74
|
+
formId: string;
|
|
75
|
+
limit?: number;
|
|
76
|
+
offset?: number;
|
|
77
|
+
}) =>
|
|
78
|
+
["formSubmissions", "list", submissionsListDiscriminator(params)] as const,
|
|
79
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports serialization helpers from getters.ts for consumers who import
|
|
3
|
+
* from @btst/stack/plugins/form-builder/api.
|
|
4
|
+
*
|
|
5
|
+
* The actual implementations live in getters.ts alongside the DB functions
|
|
6
|
+
* they serialize so they stay in sync with the returned types.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
serializeForm,
|
|
10
|
+
serializeFormSubmission,
|
|
11
|
+
serializeFormSubmissionWithData,
|
|
12
|
+
} from "./getters";
|
|
@@ -3,6 +3,7 @@ import { lazy } from "react";
|
|
|
3
3
|
import {
|
|
4
4
|
defineClientPlugin,
|
|
5
5
|
createApiClient,
|
|
6
|
+
isConnectionError,
|
|
6
7
|
} from "@btst/stack/plugins/client";
|
|
7
8
|
import { createRoute } from "@btst/yar";
|
|
8
9
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -197,6 +198,12 @@ function createFormListLoader(config: FormBuilderClientConfig) {
|
|
|
197
198
|
}
|
|
198
199
|
} catch (error) {
|
|
199
200
|
// Error hook - log the error but don't throw during SSR
|
|
201
|
+
if (isConnectionError(error)) {
|
|
202
|
+
console.warn(
|
|
203
|
+
"[btst/form-builder] route.loader() failed — no server running at build time. " +
|
|
204
|
+
"Use myStack.api.formBuilder.prefetchForRoute() for SSG data prefetching.",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
200
207
|
if (hooks?.onLoadError) {
|
|
201
208
|
await hooks.onLoadError(error as Error, context);
|
|
202
209
|
}
|
|
@@ -265,6 +272,12 @@ function createFormBuilderLoader(
|
|
|
265
272
|
}
|
|
266
273
|
} catch (error) {
|
|
267
274
|
// Error hook - log the error but don't throw during SSR
|
|
275
|
+
if (isConnectionError(error)) {
|
|
276
|
+
console.warn(
|
|
277
|
+
"[btst/form-builder] route.loader() failed — no server running at build time. " +
|
|
278
|
+
"Use myStack.api.formBuilder.prefetchForRoute() for SSG data prefetching.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
268
281
|
if (hooks?.onLoadError) {
|
|
269
282
|
await hooks.onLoadError(error as Error, context);
|
|
270
283
|
}
|
|
@@ -364,6 +377,12 @@ function createSubmissionsLoader(
|
|
|
364
377
|
}
|
|
365
378
|
} catch (error) {
|
|
366
379
|
// Error hook - log the error but don't throw during SSR
|
|
380
|
+
if (isConnectionError(error)) {
|
|
381
|
+
console.warn(
|
|
382
|
+
"[btst/form-builder] route.loader() failed — no server running at build time. " +
|
|
383
|
+
"Use myStack.api.formBuilder.prefetchForRoute() for SSG data prefetching.",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
367
386
|
if (hooks?.onLoadError) {
|
|
368
387
|
await hooks.onLoadError(error as Error, context);
|
|
369
388
|
}
|
|
@@ -10,6 +10,10 @@ import type {
|
|
|
10
10
|
PaginatedFormSubmissions,
|
|
11
11
|
SerializedFormSubmissionWithData,
|
|
12
12
|
} from "./types";
|
|
13
|
+
import {
|
|
14
|
+
formsListDiscriminator,
|
|
15
|
+
submissionsListDiscriminator,
|
|
16
|
+
} from "./api/query-key-defs";
|
|
13
17
|
|
|
14
18
|
interface FormListParams {
|
|
15
19
|
status?: "active" | "inactive" | "archived";
|
|
@@ -75,7 +79,7 @@ function createFormsQueries(
|
|
|
75
79
|
) {
|
|
76
80
|
return createQueryKeys("forms", {
|
|
77
81
|
list: (params: FormListParams = {}) => ({
|
|
78
|
-
queryKey: ["list", params],
|
|
82
|
+
queryKey: ["list", formsListDiscriminator(params)],
|
|
79
83
|
queryFn: async () => {
|
|
80
84
|
try {
|
|
81
85
|
const response: unknown = await client("/forms", {
|
|
@@ -147,7 +151,7 @@ function createSubmissionsQueries(
|
|
|
147
151
|
) {
|
|
148
152
|
return createQueryKeys("formSubmissions", {
|
|
149
153
|
list: (params: SubmissionListParams) => ({
|
|
150
|
-
queryKey: [params],
|
|
154
|
+
queryKey: [submissionsListDiscriminator(params)],
|
|
151
155
|
queryFn: async () => {
|
|
152
156
|
try {
|
|
153
157
|
const response: unknown = await client("/forms/:formId/submissions", {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
export {
|
|
2
2
|
kanbanBackendPlugin,
|
|
3
3
|
type KanbanApiRouter,
|
|
4
|
+
type KanbanRouteKey,
|
|
4
5
|
type KanbanApiContext,
|
|
5
6
|
type KanbanBackendHooks,
|
|
6
7
|
} from "./plugin";
|
|
7
8
|
export { getAllBoards, getBoardById, type BoardListResult } from "./getters";
|
|
9
|
+
export {
|
|
10
|
+
createKanbanTask,
|
|
11
|
+
findOrCreateKanbanBoard,
|
|
12
|
+
getKanbanColumnsByBoardId,
|
|
13
|
+
type CreateKanbanTaskInput,
|
|
14
|
+
} from "./mutations";
|
|
15
|
+
export { serializeBoard, serializeColumn, serializeTask } from "./serializers";
|
|
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
|
+
}
|