@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.
Files changed (179) hide show
  1. package/dist/api/index.cjs +9 -1
  2. package/dist/api/index.d.cts +4 -4
  3. package/dist/api/index.d.mts +4 -4
  4. package/dist/api/index.d.ts +4 -4
  5. package/dist/api/index.mjs +9 -1
  6. package/dist/client/index.d.cts +2 -2
  7. package/dist/client/index.d.mts +2 -2
  8. package/dist/client/index.d.ts +2 -2
  9. package/dist/index.d.cts +1 -1
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +42 -0
  13. package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +39 -0
  14. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
  15. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
  16. package/dist/packages/stack/src/plugins/blog/api/getters.cjs +131 -0
  17. package/dist/packages/stack/src/plugins/blog/api/getters.mjs +127 -0
  18. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +9 -107
  19. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +9 -107
  20. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +1 -1
  21. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +1 -1
  22. package/dist/packages/stack/src/plugins/cms/api/getters.cjs +146 -0
  23. package/dist/packages/stack/src/plugins/cms/api/getters.mjs +138 -0
  24. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +560 -622
  25. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +559 -621
  26. package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +1 -1
  27. package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +1 -1
  28. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +6 -3
  29. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +6 -3
  30. package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +111 -0
  31. package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +104 -0
  32. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +16 -88
  33. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +12 -84
  34. package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +1 -1
  35. package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +1 -1
  36. package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +84 -0
  37. package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +81 -0
  38. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +9 -123
  39. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +9 -123
  40. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +1 -1
  41. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +1 -1
  42. package/dist/plugins/ai-chat/api/index.cjs +3 -0
  43. package/dist/plugins/ai-chat/api/index.d.cts +27 -4
  44. package/dist/plugins/ai-chat/api/index.d.mts +27 -4
  45. package/dist/plugins/ai-chat/api/index.d.ts +27 -4
  46. package/dist/plugins/ai-chat/api/index.mjs +1 -0
  47. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  48. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  49. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  50. package/dist/plugins/ai-chat/query-keys.d.cts +9 -284
  51. package/dist/plugins/ai-chat/query-keys.d.mts +9 -284
  52. package/dist/plugins/ai-chat/query-keys.d.ts +9 -284
  53. package/dist/plugins/api/index.d.cts +4 -3
  54. package/dist/plugins/api/index.d.mts +4 -3
  55. package/dist/plugins/api/index.d.ts +4 -3
  56. package/dist/plugins/blog/api/index.cjs +4 -0
  57. package/dist/plugins/blog/api/index.d.cts +3 -2
  58. package/dist/plugins/blog/api/index.d.mts +3 -2
  59. package/dist/plugins/blog/api/index.d.ts +3 -2
  60. package/dist/plugins/blog/api/index.mjs +1 -0
  61. package/dist/plugins/blog/client/hooks/index.d.cts +4 -4
  62. package/dist/plugins/blog/client/hooks/index.d.mts +4 -4
  63. package/dist/plugins/blog/client/hooks/index.d.ts +4 -4
  64. package/dist/plugins/blog/client/index.d.cts +1 -1
  65. package/dist/plugins/blog/client/index.d.mts +1 -1
  66. package/dist/plugins/blog/client/index.d.ts +1 -1
  67. package/dist/plugins/blog/query-keys.cjs +7 -4
  68. package/dist/plugins/blog/query-keys.d.cts +81 -27
  69. package/dist/plugins/blog/query-keys.d.mts +81 -27
  70. package/dist/plugins/blog/query-keys.d.ts +81 -27
  71. package/dist/plugins/blog/query-keys.mjs +7 -4
  72. package/dist/plugins/client/index.d.cts +2 -2
  73. package/dist/plugins/client/index.d.mts +2 -2
  74. package/dist/plugins/client/index.d.ts +2 -2
  75. package/dist/plugins/cms/api/index.cjs +4 -0
  76. package/dist/plugins/cms/api/index.d.cts +61 -5
  77. package/dist/plugins/cms/api/index.d.mts +61 -5
  78. package/dist/plugins/cms/api/index.d.ts +61 -5
  79. package/dist/plugins/cms/api/index.mjs +1 -0
  80. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  81. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  82. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  83. package/dist/plugins/cms/query-keys.d.cts +2 -1
  84. package/dist/plugins/cms/query-keys.d.mts +2 -1
  85. package/dist/plugins/cms/query-keys.d.ts +2 -1
  86. package/dist/plugins/form-builder/api/index.cjs +4 -0
  87. package/dist/plugins/form-builder/api/index.d.cts +77 -7
  88. package/dist/plugins/form-builder/api/index.d.mts +77 -7
  89. package/dist/plugins/form-builder/api/index.d.ts +77 -7
  90. package/dist/plugins/form-builder/api/index.mjs +1 -0
  91. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  92. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  93. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  94. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  95. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  96. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  97. package/dist/plugins/form-builder/query-keys.d.cts +2 -1
  98. package/dist/plugins/form-builder/query-keys.d.mts +2 -1
  99. package/dist/plugins/form-builder/query-keys.d.ts +2 -1
  100. package/dist/plugins/kanban/api/index.cjs +3 -0
  101. package/dist/plugins/kanban/api/index.d.cts +40 -43
  102. package/dist/plugins/kanban/api/index.d.mts +40 -43
  103. package/dist/plugins/kanban/api/index.d.ts +40 -43
  104. package/dist/plugins/kanban/api/index.mjs +1 -0
  105. package/dist/plugins/kanban/client/components/index.d.cts +1 -1
  106. package/dist/plugins/kanban/client/components/index.d.mts +1 -1
  107. package/dist/plugins/kanban/client/components/index.d.ts +1 -1
  108. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  109. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  110. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  111. package/dist/plugins/kanban/client/index.d.cts +1 -1
  112. package/dist/plugins/kanban/client/index.d.mts +1 -1
  113. package/dist/plugins/kanban/client/index.d.ts +1 -1
  114. package/dist/plugins/kanban/query-keys.cjs +4 -3
  115. package/dist/plugins/kanban/query-keys.d.cts +2 -1
  116. package/dist/plugins/kanban/query-keys.d.mts +2 -1
  117. package/dist/plugins/kanban/query-keys.d.ts +2 -1
  118. package/dist/plugins/kanban/query-keys.mjs +4 -3
  119. package/dist/plugins/open-api/api/index.d.cts +2 -2
  120. package/dist/plugins/open-api/api/index.d.mts +2 -2
  121. package/dist/plugins/open-api/api/index.d.ts +2 -2
  122. package/dist/plugins/route-docs/client/index.d.cts +1 -1
  123. package/dist/plugins/route-docs/client/index.d.mts +1 -1
  124. package/dist/plugins/route-docs/client/index.d.ts +1 -1
  125. package/dist/plugins/ui-builder/index.d.cts +1 -1
  126. package/dist/plugins/ui-builder/index.d.mts +1 -1
  127. package/dist/plugins/ui-builder/index.d.ts +1 -1
  128. package/dist/shared/{stack.BoA0xkJv.d.cts → stack.7n9Y_u7N.d.cts} +33 -7
  129. package/dist/shared/{stack.BoA0xkJv.d.mts → stack.7n9Y_u7N.d.mts} +33 -7
  130. package/dist/shared/{stack.BoA0xkJv.d.ts → stack.7n9Y_u7N.d.ts} +33 -7
  131. package/dist/shared/stack.BeSm90va.d.ts +289 -0
  132. package/dist/shared/{stack.DzH_wcvr.d.mts → stack.CIrIsc-A.d.cts} +2 -2
  133. package/dist/shared/{stack.DzH_wcvr.d.ts → stack.CIrIsc-A.d.mts} +2 -2
  134. package/dist/shared/{stack.DzH_wcvr.d.cts → stack.CIrIsc-A.d.ts} +2 -2
  135. package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
  136. package/dist/shared/{stack.BsXokfNh.d.mts → stack.CXjzTMsb.d.cts} +1 -1
  137. package/dist/shared/{stack.BsXokfNh.d.ts → stack.CXjzTMsb.d.mts} +1 -1
  138. package/dist/shared/{stack.BsXokfNh.d.cts → stack.CXjzTMsb.d.ts} +1 -1
  139. package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
  140. package/dist/shared/{stack.DKDMI-QO.d.mts → stack.QD1y_7NY.d.cts} +7 -1
  141. package/dist/shared/{stack.DKDMI-QO.d.ts → stack.QD1y_7NY.d.mts} +7 -1
  142. package/dist/shared/{stack.DKDMI-QO.d.cts → stack.QD1y_7NY.d.ts} +7 -1
  143. package/package.json +1 -1
  144. package/src/__tests__/stack-api.test.ts +118 -0
  145. package/src/api/index.ts +15 -1
  146. package/src/plugins/ai-chat/__tests__/getters.test.ts +109 -0
  147. package/src/plugins/ai-chat/api/getters.ts +71 -0
  148. package/src/plugins/ai-chat/api/index.ts +1 -0
  149. package/src/plugins/ai-chat/api/plugin.ts +8 -0
  150. package/src/plugins/api/index.ts +3 -1
  151. package/src/plugins/blog/__tests__/getters.test.ts +540 -0
  152. package/src/plugins/blog/api/getters.ts +243 -0
  153. package/src/plugins/blog/api/index.ts +7 -0
  154. package/src/plugins/blog/api/plugin.ts +13 -141
  155. package/src/plugins/blog/client/plugin.tsx +2 -1
  156. package/src/plugins/blog/query-keys.ts +16 -13
  157. package/src/plugins/cms/__tests__/getters.test.ts +206 -0
  158. package/src/plugins/cms/api/getters.ts +244 -0
  159. package/src/plugins/cms/api/index.ts +5 -0
  160. package/src/plugins/cms/api/plugin.ts +50 -154
  161. package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +1 -1
  162. package/src/plugins/cms/client/hooks/cms-hooks.tsx +3 -0
  163. package/src/plugins/cms/types.ts +1 -1
  164. package/src/plugins/form-builder/__tests__/getters.test.ts +159 -0
  165. package/src/plugins/form-builder/api/getters.ts +203 -0
  166. package/src/plugins/form-builder/api/index.ts +1 -0
  167. package/src/plugins/form-builder/api/plugin.ts +22 -115
  168. package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
  169. package/src/plugins/form-builder/types.ts +2 -2
  170. package/src/plugins/kanban/__tests__/getters.test.ts +172 -0
  171. package/src/plugins/kanban/api/getters.ts +149 -0
  172. package/src/plugins/kanban/api/index.ts +1 -0
  173. package/src/plugins/kanban/api/plugin.ts +16 -146
  174. package/src/plugins/kanban/client/plugin.tsx +2 -1
  175. package/src/plugins/kanban/query-keys.ts +8 -5
  176. package/src/types.ts +44 -5
  177. package/dist/shared/{stack.CbuN2zVV.d.cts → stack.BkYlUT_8.d.cts} +6 -6
  178. package/dist/shared/{stack.CbuN2zVV.d.mts → stack.BkYlUT_8.d.mts} +6 -6
  179. 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
- * Serialize a Form for API response (convert dates to strings)
30
- */
31
- function serializeForm(form: Form): SerializedForm {
32
- return {
33
- id: form.id,
34
- name: form.name,
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
- const whereConditions: Array<{
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.findOne<Form>({
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 serializeForm(form);
123
+ return form;
191
124
  },
192
125
  );
193
126
 
@@ -647,33 +580,7 @@ export const formBuilderBackendPlugin = (
647
580
  }
648
581
  }
649
582
 
650
- // Get total count
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
+ }
@@ -4,3 +4,4 @@ export {
4
4
  type KanbanApiContext,
5
5
  type KanbanBackendHooks,
6
6
  } from "./plugin";
7
+ export { getAllBoards, getBoardById, type BoardListResult } from "./getters";