@btst/stack 2.1.0 → 2.2.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/api/index.cjs +9 -1
- package/dist/api/index.d.cts +4 -4
- package/dist/api/index.d.mts +4 -4
- package/dist/api/index.d.ts +4 -4
- package/dist/api/index.mjs +9 -1
- package/dist/client/index.d.cts +2 -2
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +42 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +39 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
- package/dist/packages/stack/src/plugins/blog/api/getters.cjs +131 -0
- package/dist/packages/stack/src/plugins/blog/api/getters.mjs +127 -0
- package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +9 -107
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +9 -107
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +1 -1
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +1 -1
- package/dist/packages/stack/src/plugins/cms/api/getters.cjs +146 -0
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +138 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +560 -622
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +559 -621
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +1 -1
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +1 -1
- package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +6 -3
- package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +6 -3
- package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +111 -0
- package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +104 -0
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +16 -88
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +12 -84
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +1 -1
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +84 -0
- package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +81 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +9 -123
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +9 -123
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +1 -1
- package/dist/plugins/ai-chat/api/index.cjs +3 -0
- package/dist/plugins/ai-chat/api/index.d.cts +27 -4
- package/dist/plugins/ai-chat/api/index.d.mts +27 -4
- package/dist/plugins/ai-chat/api/index.d.ts +27 -4
- package/dist/plugins/ai-chat/api/index.mjs +1 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
- package/dist/plugins/ai-chat/query-keys.d.cts +9 -284
- package/dist/plugins/ai-chat/query-keys.d.mts +9 -284
- package/dist/plugins/ai-chat/query-keys.d.ts +9 -284
- package/dist/plugins/api/index.d.cts +4 -3
- package/dist/plugins/api/index.d.mts +4 -3
- package/dist/plugins/api/index.d.ts +4 -3
- package/dist/plugins/blog/api/index.cjs +4 -0
- package/dist/plugins/blog/api/index.d.cts +3 -2
- package/dist/plugins/blog/api/index.d.mts +3 -2
- package/dist/plugins/blog/api/index.d.ts +3 -2
- package/dist/plugins/blog/api/index.mjs +1 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +4 -4
- package/dist/plugins/blog/client/hooks/index.d.mts +4 -4
- package/dist/plugins/blog/client/hooks/index.d.ts +4 -4
- 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 +7 -4
- package/dist/plugins/blog/query-keys.d.cts +81 -27
- package/dist/plugins/blog/query-keys.d.mts +81 -27
- package/dist/plugins/blog/query-keys.d.ts +81 -27
- package/dist/plugins/blog/query-keys.mjs +7 -4
- package/dist/plugins/client/index.d.cts +2 -2
- package/dist/plugins/client/index.d.mts +2 -2
- package/dist/plugins/client/index.d.ts +2 -2
- package/dist/plugins/cms/api/index.cjs +4 -0
- package/dist/plugins/cms/api/index.d.cts +61 -5
- package/dist/plugins/cms/api/index.d.mts +61 -5
- package/dist/plugins/cms/api/index.d.ts +61 -5
- 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/query-keys.d.cts +2 -1
- package/dist/plugins/cms/query-keys.d.mts +2 -1
- package/dist/plugins/cms/query-keys.d.ts +2 -1
- package/dist/plugins/form-builder/api/index.cjs +4 -0
- package/dist/plugins/form-builder/api/index.d.cts +77 -7
- package/dist/plugins/form-builder/api/index.d.mts +77 -7
- package/dist/plugins/form-builder/api/index.d.ts +77 -7
- package/dist/plugins/form-builder/api/index.mjs +1 -0
- 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.d.cts +2 -1
- package/dist/plugins/form-builder/query-keys.d.mts +2 -1
- package/dist/plugins/form-builder/query-keys.d.ts +2 -1
- package/dist/plugins/kanban/api/index.cjs +3 -0
- package/dist/plugins/kanban/api/index.d.cts +40 -43
- package/dist/plugins/kanban/api/index.d.mts +40 -43
- package/dist/plugins/kanban/api/index.d.ts +40 -43
- package/dist/plugins/kanban/api/index.mjs +1 -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 +4 -3
- package/dist/plugins/kanban/query-keys.d.cts +2 -1
- package/dist/plugins/kanban/query-keys.d.mts +2 -1
- package/dist/plugins/kanban/query-keys.d.ts +2 -1
- package/dist/plugins/kanban/query-keys.mjs +4 -3
- package/dist/plugins/open-api/api/index.d.cts +2 -2
- package/dist/plugins/open-api/api/index.d.mts +2 -2
- package/dist/plugins/open-api/api/index.d.ts +2 -2
- package/dist/plugins/route-docs/client/index.d.cts +1 -1
- package/dist/plugins/route-docs/client/index.d.mts +1 -1
- package/dist/plugins/route-docs/client/index.d.ts +1 -1
- 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.BoA0xkJv.d.cts → stack.7n9Y_u7N.d.cts} +33 -7
- package/dist/shared/{stack.BoA0xkJv.d.mts → stack.7n9Y_u7N.d.mts} +33 -7
- package/dist/shared/{stack.BoA0xkJv.d.ts → stack.7n9Y_u7N.d.ts} +33 -7
- package/dist/shared/stack.BeSm90va.d.ts +289 -0
- package/dist/shared/{stack.DzH_wcvr.d.mts → stack.CIrIsc-A.d.cts} +2 -2
- package/dist/shared/{stack.DzH_wcvr.d.ts → stack.CIrIsc-A.d.mts} +2 -2
- package/dist/shared/{stack.DzH_wcvr.d.cts → stack.CIrIsc-A.d.ts} +2 -2
- package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
- package/dist/shared/{stack.BsXokfNh.d.mts → stack.CXjzTMsb.d.cts} +1 -1
- package/dist/shared/{stack.BsXokfNh.d.ts → stack.CXjzTMsb.d.mts} +1 -1
- package/dist/shared/{stack.BsXokfNh.d.cts → stack.CXjzTMsb.d.ts} +1 -1
- package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
- package/dist/shared/{stack.DKDMI-QO.d.mts → stack.QD1y_7NY.d.cts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.ts → stack.QD1y_7NY.d.mts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.cts → stack.QD1y_7NY.d.ts} +7 -1
- package/package.json +1 -1
- package/src/__tests__/stack-api.test.ts +118 -0
- package/src/api/index.ts +15 -1
- package/src/plugins/ai-chat/__tests__/getters.test.ts +109 -0
- package/src/plugins/ai-chat/api/getters.ts +71 -0
- package/src/plugins/ai-chat/api/index.ts +1 -0
- package/src/plugins/ai-chat/api/plugin.ts +8 -0
- package/src/plugins/api/index.ts +3 -1
- package/src/plugins/blog/__tests__/getters.test.ts +540 -0
- package/src/plugins/blog/api/getters.ts +243 -0
- package/src/plugins/blog/api/index.ts +7 -0
- package/src/plugins/blog/api/plugin.ts +13 -141
- package/src/plugins/blog/client/plugin.tsx +2 -1
- package/src/plugins/blog/query-keys.ts +16 -13
- package/src/plugins/cms/__tests__/getters.test.ts +206 -0
- package/src/plugins/cms/api/getters.ts +244 -0
- package/src/plugins/cms/api/index.ts +5 -0
- package/src/plugins/cms/api/plugin.ts +50 -154
- package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +1 -1
- package/src/plugins/cms/client/hooks/cms-hooks.tsx +3 -0
- package/src/plugins/cms/types.ts +1 -1
- package/src/plugins/form-builder/__tests__/getters.test.ts +159 -0
- package/src/plugins/form-builder/api/getters.ts +203 -0
- package/src/plugins/form-builder/api/index.ts +1 -0
- package/src/plugins/form-builder/api/plugin.ts +22 -115
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
- package/src/plugins/form-builder/types.ts +2 -2
- package/src/plugins/kanban/__tests__/getters.test.ts +172 -0
- package/src/plugins/kanban/api/getters.ts +149 -0
- package/src/plugins/kanban/api/index.ts +1 -0
- package/src/plugins/kanban/api/plugin.ts +16 -146
- package/src/plugins/kanban/client/plugin.tsx +2 -1
- package/src/plugins/kanban/query-keys.ts +8 -5
- package/src/types.ts +44 -5
- package/dist/shared/{stack.CbuN2zVV.d.cts → stack.BkYlUT_8.d.cts} +6 -6
- package/dist/shared/{stack.CbuN2zVV.d.mts → stack.BkYlUT_8.d.mts} +6 -6
- package/dist/shared/{stack.CbuN2zVV.d.ts → stack.BkYlUT_8.d.ts} +6 -6
|
@@ -11,9 +11,6 @@ import type {
|
|
|
11
11
|
FormBuilderBackendConfig,
|
|
12
12
|
FormBuilderHookContext,
|
|
13
13
|
SubmissionHookContext,
|
|
14
|
-
SerializedForm,
|
|
15
|
-
SerializedFormSubmission,
|
|
16
|
-
SerializedFormSubmissionWithData,
|
|
17
14
|
FormInput,
|
|
18
15
|
FormUpdate,
|
|
19
16
|
} from "../types";
|
|
@@ -24,50 +21,14 @@ import {
|
|
|
24
21
|
listSubmissionsQuerySchema,
|
|
25
22
|
} from "../schemas";
|
|
26
23
|
import { slugify, extractIpAddress, extractUserAgent } from "../utils";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
slug: form.slug,
|
|
36
|
-
description: form.description,
|
|
37
|
-
schema: form.schema,
|
|
38
|
-
successMessage: form.successMessage,
|
|
39
|
-
redirectUrl: form.redirectUrl,
|
|
40
|
-
status: form.status,
|
|
41
|
-
createdBy: form.createdBy,
|
|
42
|
-
createdAt: form.createdAt.toISOString(),
|
|
43
|
-
updatedAt: form.updatedAt.toISOString(),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Serialize a FormSubmission for API response (convert dates to strings)
|
|
49
|
-
*/
|
|
50
|
-
function serializeFormSubmission(
|
|
51
|
-
submission: FormSubmission,
|
|
52
|
-
): SerializedFormSubmission {
|
|
53
|
-
return {
|
|
54
|
-
...submission,
|
|
55
|
-
submittedAt: submission.submittedAt.toISOString(),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Serialize a FormSubmission with parsed data and joined Form
|
|
61
|
-
*/
|
|
62
|
-
function serializeFormSubmissionWithData(
|
|
63
|
-
submission: FormSubmissionWithForm,
|
|
64
|
-
): SerializedFormSubmissionWithData {
|
|
65
|
-
return {
|
|
66
|
-
...serializeFormSubmission(submission),
|
|
67
|
-
parsedData: JSON.parse(submission.data),
|
|
68
|
-
form: submission.form ? serializeForm(submission.form) : undefined,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
24
|
+
import {
|
|
25
|
+
getAllForms,
|
|
26
|
+
getFormBySlug as getFormBySlugFromDb,
|
|
27
|
+
getFormSubmissions,
|
|
28
|
+
serializeForm,
|
|
29
|
+
serializeFormSubmission,
|
|
30
|
+
serializeFormSubmissionWithData,
|
|
31
|
+
} from "./getters";
|
|
71
32
|
|
|
72
33
|
/**
|
|
73
34
|
* Form Builder backend plugin
|
|
@@ -83,6 +44,16 @@ export const formBuilderBackendPlugin = (
|
|
|
83
44
|
|
|
84
45
|
dbPlugin: dbSchema,
|
|
85
46
|
|
|
47
|
+
api: (adapter) => ({
|
|
48
|
+
getAllForms: (params?: Parameters<typeof getAllForms>[1]) =>
|
|
49
|
+
getAllForms(adapter, params),
|
|
50
|
+
getFormBySlug: (slug: string) => getFormBySlugFromDb(adapter, slug),
|
|
51
|
+
getFormSubmissions: (
|
|
52
|
+
formId: string,
|
|
53
|
+
params?: Parameters<typeof getFormSubmissions>[2],
|
|
54
|
+
) => getFormSubmissions(adapter, formId, params),
|
|
55
|
+
}),
|
|
56
|
+
|
|
86
57
|
routes: (adapter: Adapter) => {
|
|
87
58
|
// Helper to create hook context from request
|
|
88
59
|
const createContext = (headers?: Headers): FormBuilderHookContext => ({
|
|
@@ -114,7 +85,6 @@ export const formBuilderBackendPlugin = (
|
|
|
114
85
|
const { status, limit, offset } = ctx.query;
|
|
115
86
|
const context = createContext(ctx.headers);
|
|
116
87
|
|
|
117
|
-
// Call before hook for auth check
|
|
118
88
|
if (config.hooks?.onBeforeListForms) {
|
|
119
89
|
const canList = await config.hooks.onBeforeListForms(context);
|
|
120
90
|
if (!canList) {
|
|
@@ -122,41 +92,7 @@ export const formBuilderBackendPlugin = (
|
|
|
122
92
|
}
|
|
123
93
|
}
|
|
124
94
|
|
|
125
|
-
|
|
126
|
-
field: string;
|
|
127
|
-
value: string;
|
|
128
|
-
operator: "eq";
|
|
129
|
-
}> = [];
|
|
130
|
-
if (status) {
|
|
131
|
-
whereConditions.push({
|
|
132
|
-
field: "status",
|
|
133
|
-
value: status,
|
|
134
|
-
operator: "eq" as const,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Get total count
|
|
139
|
-
const allForms = await adapter.findMany<Form>({
|
|
140
|
-
model: "form",
|
|
141
|
-
where: whereConditions.length > 0 ? whereConditions : undefined,
|
|
142
|
-
});
|
|
143
|
-
const total = allForms.length;
|
|
144
|
-
|
|
145
|
-
// Get paginated forms
|
|
146
|
-
const forms = await adapter.findMany<Form>({
|
|
147
|
-
model: "form",
|
|
148
|
-
where: whereConditions.length > 0 ? whereConditions : undefined,
|
|
149
|
-
limit,
|
|
150
|
-
offset,
|
|
151
|
-
sortBy: { field: "createdAt", direction: "desc" },
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
items: forms.map(serializeForm),
|
|
156
|
-
total,
|
|
157
|
-
limit,
|
|
158
|
-
offset,
|
|
159
|
-
};
|
|
95
|
+
return getAllForms(adapter, { status, limit, offset });
|
|
160
96
|
},
|
|
161
97
|
);
|
|
162
98
|
|
|
@@ -178,16 +114,13 @@ export const formBuilderBackendPlugin = (
|
|
|
178
114
|
}
|
|
179
115
|
}
|
|
180
116
|
|
|
181
|
-
const form = await adapter
|
|
182
|
-
model: "form",
|
|
183
|
-
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
184
|
-
});
|
|
117
|
+
const form = await getFormBySlugFromDb(adapter, slug);
|
|
185
118
|
|
|
186
119
|
if (!form) {
|
|
187
120
|
throw ctx.error(404, { message: "Form not found" });
|
|
188
121
|
}
|
|
189
122
|
|
|
190
|
-
return
|
|
123
|
+
return form;
|
|
191
124
|
},
|
|
192
125
|
);
|
|
193
126
|
|
|
@@ -647,33 +580,7 @@ export const formBuilderBackendPlugin = (
|
|
|
647
580
|
}
|
|
648
581
|
}
|
|
649
582
|
|
|
650
|
-
|
|
651
|
-
const allSubmissions = await adapter.findMany<FormSubmission>({
|
|
652
|
-
model: "formSubmission",
|
|
653
|
-
where: [
|
|
654
|
-
{ field: "formId", value: formId, operator: "eq" as const },
|
|
655
|
-
],
|
|
656
|
-
});
|
|
657
|
-
const total = allSubmissions.length;
|
|
658
|
-
|
|
659
|
-
// Get paginated submissions
|
|
660
|
-
const submissions = await adapter.findMany<FormSubmissionWithForm>({
|
|
661
|
-
model: "formSubmission",
|
|
662
|
-
where: [
|
|
663
|
-
{ field: "formId", value: formId, operator: "eq" as const },
|
|
664
|
-
],
|
|
665
|
-
limit,
|
|
666
|
-
offset,
|
|
667
|
-
sortBy: { field: "submittedAt", direction: "desc" },
|
|
668
|
-
join: { form: true },
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
return {
|
|
672
|
-
items: submissions.map(serializeFormSubmissionWithData),
|
|
673
|
-
total,
|
|
674
|
-
limit,
|
|
675
|
-
offset,
|
|
676
|
-
};
|
|
583
|
+
return getFormSubmissions(adapter, formId, { limit, offset });
|
|
677
584
|
},
|
|
678
585
|
);
|
|
679
586
|
|
|
@@ -147,7 +147,7 @@ export function SubmissionsPage({ formId }: SubmissionsPageProps) {
|
|
|
147
147
|
{sub.id.slice(0, 8)}...
|
|
148
148
|
</TableCell>
|
|
149
149
|
<TableCell className="max-w-xs truncate text-sm text-muted-foreground">
|
|
150
|
-
{formatSubmissionData(sub.parsedData)}
|
|
150
|
+
{formatSubmissionData(sub.parsedData ?? {})}
|
|
151
151
|
</TableCell>
|
|
152
152
|
<TableCell className="text-muted-foreground">
|
|
153
153
|
{new Date(sub.submittedAt).toLocaleString()}
|
|
@@ -82,8 +82,8 @@ export interface SerializedFormSubmission
|
|
|
82
82
|
export interface SerializedFormSubmissionWithData<
|
|
83
83
|
TData = Record<string, unknown>,
|
|
84
84
|
> extends SerializedFormSubmission {
|
|
85
|
-
/** Parsed data object (JSON.parse of data field) */
|
|
86
|
-
parsedData: TData;
|
|
85
|
+
/** Parsed data object (JSON.parse of data field). Null when the stored JSON is corrupted. */
|
|
86
|
+
parsedData: TData | null;
|
|
87
87
|
/** Joined form */
|
|
88
88
|
form?: SerializedForm;
|
|
89
89
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
3
|
+
import { defineDb } from "@btst/db";
|
|
4
|
+
import type { Adapter } from "@btst/db";
|
|
5
|
+
import { kanbanSchema } from "../db";
|
|
6
|
+
import { getAllBoards, getBoardById } from "../api/getters";
|
|
7
|
+
|
|
8
|
+
const createTestAdapter = (): Adapter => {
|
|
9
|
+
const db = defineDb({}).use(kanbanSchema);
|
|
10
|
+
return createMemoryAdapter(db)({});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function createBoard(
|
|
14
|
+
adapter: Adapter,
|
|
15
|
+
name: string,
|
|
16
|
+
slug: string,
|
|
17
|
+
ownerId?: string,
|
|
18
|
+
): Promise<any> {
|
|
19
|
+
return adapter.create({
|
|
20
|
+
model: "kanbanBoard",
|
|
21
|
+
data: {
|
|
22
|
+
name,
|
|
23
|
+
slug,
|
|
24
|
+
...(ownerId ? { ownerId } : {}),
|
|
25
|
+
createdAt: new Date(),
|
|
26
|
+
updatedAt: new Date(),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function createColumn(
|
|
32
|
+
adapter: Adapter,
|
|
33
|
+
boardId: string,
|
|
34
|
+
title: string,
|
|
35
|
+
order: number,
|
|
36
|
+
): Promise<any> {
|
|
37
|
+
return adapter.create({
|
|
38
|
+
model: "kanbanColumn",
|
|
39
|
+
data: {
|
|
40
|
+
boardId,
|
|
41
|
+
title,
|
|
42
|
+
order,
|
|
43
|
+
createdAt: new Date(),
|
|
44
|
+
updatedAt: new Date(),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function createTask(
|
|
50
|
+
adapter: Adapter,
|
|
51
|
+
columnId: string,
|
|
52
|
+
title: string,
|
|
53
|
+
order: number,
|
|
54
|
+
): Promise<any> {
|
|
55
|
+
return adapter.create({
|
|
56
|
+
model: "kanbanTask",
|
|
57
|
+
data: {
|
|
58
|
+
columnId,
|
|
59
|
+
title,
|
|
60
|
+
priority: "MEDIUM",
|
|
61
|
+
order,
|
|
62
|
+
isArchived: false,
|
|
63
|
+
createdAt: new Date(),
|
|
64
|
+
updatedAt: new Date(),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("kanban getters", () => {
|
|
70
|
+
let adapter: Adapter;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
adapter = createTestAdapter();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("getAllBoards", () => {
|
|
77
|
+
it("returns empty array when no boards exist", async () => {
|
|
78
|
+
const { items, total } = await getAllBoards(adapter);
|
|
79
|
+
expect(items).toEqual([]);
|
|
80
|
+
expect(total).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns all boards with columns and tasks", async () => {
|
|
84
|
+
const board = (await createBoard(adapter, "My Board", "my-board")) as any;
|
|
85
|
+
const col = (await createColumn(adapter, board.id, "To Do", 0)) as any;
|
|
86
|
+
await createTask(adapter, col.id, "Task 1", 0);
|
|
87
|
+
|
|
88
|
+
const { items: boards, total } = await getAllBoards(adapter);
|
|
89
|
+
expect(boards).toHaveLength(1);
|
|
90
|
+
expect(total).toBe(1);
|
|
91
|
+
expect(boards[0]!.slug).toBe("my-board");
|
|
92
|
+
expect(boards[0]!.columns).toHaveLength(1);
|
|
93
|
+
expect(boards[0]!.columns[0]!.title).toBe("To Do");
|
|
94
|
+
expect(boards[0]!.columns[0]!.tasks).toHaveLength(1);
|
|
95
|
+
expect(boards[0]!.columns[0]!.tasks[0]!.title).toBe("Task 1");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns boards with empty columns array when no columns exist", async () => {
|
|
99
|
+
await createBoard(adapter, "Empty Board", "empty-board");
|
|
100
|
+
|
|
101
|
+
const { items: boards } = await getAllBoards(adapter);
|
|
102
|
+
expect(boards).toHaveLength(1);
|
|
103
|
+
expect(boards[0]!.columns).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("filters boards by slug", async () => {
|
|
107
|
+
await createBoard(adapter, "Board A", "board-a");
|
|
108
|
+
await createBoard(adapter, "Board B", "board-b");
|
|
109
|
+
|
|
110
|
+
const { items, total } = await getAllBoards(adapter, { slug: "board-a" });
|
|
111
|
+
expect(items).toHaveLength(1);
|
|
112
|
+
expect(total).toBe(1);
|
|
113
|
+
expect(items[0]!.slug).toBe("board-a");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("filters boards by ownerId", async () => {
|
|
117
|
+
await createBoard(adapter, "Alice Board", "alice-board", "user-alice");
|
|
118
|
+
await createBoard(adapter, "Bob Board", "bob-board", "user-bob");
|
|
119
|
+
|
|
120
|
+
const { items, total } = await getAllBoards(adapter, {
|
|
121
|
+
ownerId: "user-alice",
|
|
122
|
+
});
|
|
123
|
+
expect(items).toHaveLength(1);
|
|
124
|
+
expect(total).toBe(1);
|
|
125
|
+
expect(items[0]!.slug).toBe("alice-board");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("sorts columns by order", async () => {
|
|
129
|
+
const board = (await createBoard(adapter, "Board", "board")) as any;
|
|
130
|
+
// Create columns out of order
|
|
131
|
+
await createColumn(adapter, board.id, "Done", 2);
|
|
132
|
+
await createColumn(adapter, board.id, "To Do", 0);
|
|
133
|
+
await createColumn(adapter, board.id, "In Progress", 1);
|
|
134
|
+
|
|
135
|
+
const { items: boards } = await getAllBoards(adapter);
|
|
136
|
+
expect(boards[0]!.columns[0]!.title).toBe("To Do");
|
|
137
|
+
expect(boards[0]!.columns[1]!.title).toBe("In Progress");
|
|
138
|
+
expect(boards[0]!.columns[2]!.title).toBe("Done");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("getBoardById", () => {
|
|
143
|
+
it("returns null when board does not exist", async () => {
|
|
144
|
+
const board = await getBoardById(adapter, "nonexistent");
|
|
145
|
+
expect(board).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns the board with columns and tasks", async () => {
|
|
149
|
+
const board = (await createBoard(adapter, "My Board", "my-board")) as any;
|
|
150
|
+
const col1 = (await createColumn(adapter, board.id, "To Do", 0)) as any;
|
|
151
|
+
const col2 = (await createColumn(adapter, board.id, "Done", 1)) as any;
|
|
152
|
+
await createTask(adapter, col1.id, "Task A", 0);
|
|
153
|
+
await createTask(adapter, col1.id, "Task B", 1);
|
|
154
|
+
await createTask(adapter, col2.id, "Task C", 0);
|
|
155
|
+
|
|
156
|
+
const result = await getBoardById(adapter, board.id);
|
|
157
|
+
expect(result).not.toBeNull();
|
|
158
|
+
expect(result!.id).toBe(board.id);
|
|
159
|
+
expect(result!.columns).toHaveLength(2);
|
|
160
|
+
expect(result!.columns[0]!.tasks).toHaveLength(2);
|
|
161
|
+
expect(result!.columns[1]!.tasks).toHaveLength(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns board with empty columns when no columns exist", async () => {
|
|
165
|
+
const board = (await createBoard(adapter, "Empty Board", "empty")) as any;
|
|
166
|
+
|
|
167
|
+
const result = await getBoardById(adapter, board.id);
|
|
168
|
+
expect(result).not.toBeNull();
|
|
169
|
+
expect(result!.columns).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import type {
|
|
3
|
+
BoardWithKanbanColumn,
|
|
4
|
+
BoardWithColumns,
|
|
5
|
+
ColumnWithTasks,
|
|
6
|
+
Task,
|
|
7
|
+
} from "../types";
|
|
8
|
+
import type { z } from "zod";
|
|
9
|
+
import type { BoardListQuerySchema } from "../schemas";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Paginated result returned by {@link getAllBoards}.
|
|
13
|
+
*/
|
|
14
|
+
export interface BoardListResult {
|
|
15
|
+
items: BoardWithColumns[];
|
|
16
|
+
total: number;
|
|
17
|
+
limit?: number;
|
|
18
|
+
offset?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Given a raw board record (with a `column` join), fetches tasks for every
|
|
23
|
+
* column in parallel and returns the sorted columns with their tasks attached.
|
|
24
|
+
* Strips the raw `column` join field from the returned board object.
|
|
25
|
+
*/
|
|
26
|
+
async function hydrateColumnsWithTasks(
|
|
27
|
+
adapter: Adapter,
|
|
28
|
+
board: BoardWithKanbanColumn,
|
|
29
|
+
): Promise<BoardWithColumns> {
|
|
30
|
+
const columnIds = (board.column || []).map((c) => c.id);
|
|
31
|
+
const tasksByColumn = new Map<string, Task[]>();
|
|
32
|
+
|
|
33
|
+
if (columnIds.length > 0) {
|
|
34
|
+
const taskResults = await Promise.all(
|
|
35
|
+
columnIds.map((columnId) =>
|
|
36
|
+
adapter.findMany<Task>({
|
|
37
|
+
model: "kanbanTask",
|
|
38
|
+
where: [
|
|
39
|
+
{ field: "columnId", value: columnId, operator: "eq" as const },
|
|
40
|
+
],
|
|
41
|
+
sortBy: { field: "order", direction: "asc" },
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
for (let i = 0; i < columnIds.length; i++) {
|
|
46
|
+
const columnId = columnIds[i];
|
|
47
|
+
const tasks = taskResults[i];
|
|
48
|
+
if (columnId && tasks) {
|
|
49
|
+
tasksByColumn.set(columnId, tasks);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const columns: ColumnWithTasks[] = (board.column || [])
|
|
55
|
+
.sort((a, b) => a.order - b.order)
|
|
56
|
+
.map((col) => ({ ...col, tasks: tasksByColumn.get(col.id) || [] }));
|
|
57
|
+
|
|
58
|
+
const { column: _, ...boardWithoutJoin } = board;
|
|
59
|
+
return { ...boardWithoutJoin, columns };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Retrieve all boards matching optional filter criteria, with columns and tasks.
|
|
64
|
+
* Pure DB function - no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
65
|
+
*
|
|
66
|
+
* @param adapter - The database adapter
|
|
67
|
+
* @param params - Optional filter/pagination parameters (same shape as the list API query)
|
|
68
|
+
*/
|
|
69
|
+
export async function getAllBoards(
|
|
70
|
+
adapter: Adapter,
|
|
71
|
+
params?: z.infer<typeof BoardListQuerySchema>,
|
|
72
|
+
): Promise<BoardListResult> {
|
|
73
|
+
const query = params ?? {};
|
|
74
|
+
|
|
75
|
+
const whereConditions: Array<{
|
|
76
|
+
field: string;
|
|
77
|
+
value: string;
|
|
78
|
+
operator: "eq";
|
|
79
|
+
}> = [];
|
|
80
|
+
|
|
81
|
+
if (query.slug) {
|
|
82
|
+
whereConditions.push({
|
|
83
|
+
field: "slug",
|
|
84
|
+
value: query.slug,
|
|
85
|
+
operator: "eq" as const,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (query.ownerId) {
|
|
90
|
+
whereConditions.push({
|
|
91
|
+
field: "ownerId",
|
|
92
|
+
value: query.ownerId,
|
|
93
|
+
operator: "eq" as const,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (query.organizationId) {
|
|
98
|
+
whereConditions.push({
|
|
99
|
+
field: "organizationId",
|
|
100
|
+
value: query.organizationId,
|
|
101
|
+
operator: "eq" as const,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const where = whereConditions.length > 0 ? whereConditions : undefined;
|
|
106
|
+
|
|
107
|
+
const [boards, total] = await Promise.all([
|
|
108
|
+
adapter.findMany<BoardWithKanbanColumn>({
|
|
109
|
+
model: "kanbanBoard",
|
|
110
|
+
limit: query.limit ?? 50,
|
|
111
|
+
offset: query.offset ?? 0,
|
|
112
|
+
where,
|
|
113
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
114
|
+
join: { kanbanColumn: true },
|
|
115
|
+
}),
|
|
116
|
+
adapter.count({ model: "kanbanBoard", where }),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const items = await Promise.all(
|
|
120
|
+
boards.map((board) => hydrateColumnsWithTasks(adapter, board)),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return { items, total, limit: query.limit, offset: query.offset };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Retrieve a single board by its ID, with all columns and tasks.
|
|
128
|
+
* Returns null if the board is not found.
|
|
129
|
+
* Pure DB function - no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
130
|
+
*
|
|
131
|
+
* @param adapter - The database adapter
|
|
132
|
+
* @param id - The board ID
|
|
133
|
+
*/
|
|
134
|
+
export async function getBoardById(
|
|
135
|
+
adapter: Adapter,
|
|
136
|
+
id: string,
|
|
137
|
+
): Promise<BoardWithColumns | null> {
|
|
138
|
+
const board = await adapter.findOne<BoardWithKanbanColumn>({
|
|
139
|
+
model: "kanbanBoard",
|
|
140
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
141
|
+
join: { kanbanColumn: true },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!board) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return hydrateColumnsWithTasks(adapter, board);
|
|
149
|
+
}
|