@btst/stack 2.1.0 → 2.3.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 (229) 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 +60 -107
  19. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +60 -107
  20. package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
  21. package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
  22. package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
  23. package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
  24. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +16 -1
  25. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +17 -2
  26. package/dist/packages/stack/src/plugins/cms/api/getters.cjs +156 -0
  27. package/dist/packages/stack/src/plugins/cms/api/getters.mjs +147 -0
  28. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +624 -617
  29. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +623 -616
  30. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
  31. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
  32. package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +1 -1
  33. package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +1 -1
  34. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +6 -3
  35. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +6 -3
  36. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +15 -0
  37. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
  38. package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +120 -0
  39. package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +112 -0
  40. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +75 -86
  41. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +71 -82
  42. package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
  43. package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
  44. package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +1 -1
  45. package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +1 -1
  46. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +15 -0
  47. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -1
  48. package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +84 -0
  49. package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +81 -0
  50. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +37 -123
  51. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +37 -123
  52. package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
  53. package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
  54. package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
  55. package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
  56. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +11 -1
  57. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +12 -2
  58. package/dist/packages/stack/src/plugins/utils.cjs +6 -0
  59. package/dist/packages/stack/src/plugins/utils.mjs +6 -1
  60. package/dist/plugins/ai-chat/api/index.cjs +3 -0
  61. package/dist/plugins/ai-chat/api/index.d.cts +27 -4
  62. package/dist/plugins/ai-chat/api/index.d.mts +27 -4
  63. package/dist/plugins/ai-chat/api/index.d.ts +27 -4
  64. package/dist/plugins/ai-chat/api/index.mjs +1 -0
  65. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  66. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  67. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  68. package/dist/plugins/ai-chat/query-keys.d.cts +9 -284
  69. package/dist/plugins/ai-chat/query-keys.d.mts +9 -284
  70. package/dist/plugins/ai-chat/query-keys.d.ts +9 -284
  71. package/dist/plugins/api/index.d.cts +4 -3
  72. package/dist/plugins/api/index.d.mts +4 -3
  73. package/dist/plugins/api/index.d.ts +4 -3
  74. package/dist/plugins/blog/api/index.cjs +9 -0
  75. package/dist/plugins/blog/api/index.d.cts +20 -4
  76. package/dist/plugins/blog/api/index.d.mts +20 -4
  77. package/dist/plugins/blog/api/index.d.ts +20 -4
  78. package/dist/plugins/blog/api/index.mjs +3 -0
  79. package/dist/plugins/blog/client/hooks/index.d.cts +5 -5
  80. package/dist/plugins/blog/client/hooks/index.d.mts +5 -5
  81. package/dist/plugins/blog/client/hooks/index.d.ts +5 -5
  82. package/dist/plugins/blog/client/index.d.cts +1 -1
  83. package/dist/plugins/blog/client/index.d.mts +1 -1
  84. package/dist/plugins/blog/client/index.d.ts +1 -1
  85. package/dist/plugins/blog/query-keys.cjs +13 -9
  86. package/dist/plugins/blog/query-keys.d.cts +8 -333
  87. package/dist/plugins/blog/query-keys.d.mts +8 -333
  88. package/dist/plugins/blog/query-keys.d.ts +8 -333
  89. package/dist/plugins/blog/query-keys.mjs +13 -9
  90. package/dist/plugins/client/index.cjs +1 -0
  91. package/dist/plugins/client/index.d.cts +10 -3
  92. package/dist/plugins/client/index.d.mts +10 -3
  93. package/dist/plugins/client/index.d.ts +10 -3
  94. package/dist/plugins/client/index.mjs +1 -1
  95. package/dist/plugins/cms/api/index.cjs +10 -0
  96. package/dist/plugins/cms/api/index.d.cts +7 -163
  97. package/dist/plugins/cms/api/index.d.mts +7 -163
  98. package/dist/plugins/cms/api/index.d.ts +7 -163
  99. package/dist/plugins/cms/api/index.mjs +2 -0
  100. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  101. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  102. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  103. package/dist/plugins/cms/query-keys.cjs +2 -1
  104. package/dist/plugins/cms/query-keys.d.cts +6 -9
  105. package/dist/plugins/cms/query-keys.d.mts +6 -9
  106. package/dist/plugins/cms/query-keys.d.ts +6 -9
  107. package/dist/plugins/cms/query-keys.mjs +2 -1
  108. package/dist/plugins/form-builder/api/index.cjs +10 -0
  109. package/dist/plugins/form-builder/api/index.d.cts +7 -141
  110. package/dist/plugins/form-builder/api/index.d.mts +7 -141
  111. package/dist/plugins/form-builder/api/index.d.ts +7 -141
  112. package/dist/plugins/form-builder/api/index.mjs +2 -0
  113. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  114. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  115. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  116. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  117. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  118. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  119. package/dist/plugins/form-builder/query-keys.cjs +3 -2
  120. package/dist/plugins/form-builder/query-keys.d.cts +7 -6
  121. package/dist/plugins/form-builder/query-keys.d.mts +7 -6
  122. package/dist/plugins/form-builder/query-keys.d.ts +7 -6
  123. package/dist/plugins/form-builder/query-keys.mjs +3 -2
  124. package/dist/plugins/kanban/api/index.cjs +9 -0
  125. package/dist/plugins/kanban/api/index.d.cts +17 -395
  126. package/dist/plugins/kanban/api/index.d.mts +17 -395
  127. package/dist/plugins/kanban/api/index.d.ts +17 -395
  128. package/dist/plugins/kanban/api/index.mjs +3 -0
  129. package/dist/plugins/kanban/client/components/index.d.cts +1 -1
  130. package/dist/plugins/kanban/client/components/index.d.mts +1 -1
  131. package/dist/plugins/kanban/client/components/index.d.ts +1 -1
  132. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  133. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  134. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  135. package/dist/plugins/kanban/client/index.d.cts +1 -1
  136. package/dist/plugins/kanban/client/index.d.mts +1 -1
  137. package/dist/plugins/kanban/client/index.d.ts +1 -1
  138. package/dist/plugins/kanban/query-keys.cjs +6 -12
  139. package/dist/plugins/kanban/query-keys.d.cts +5 -16
  140. package/dist/plugins/kanban/query-keys.d.mts +5 -16
  141. package/dist/plugins/kanban/query-keys.d.ts +5 -16
  142. package/dist/plugins/kanban/query-keys.mjs +6 -12
  143. package/dist/plugins/open-api/api/index.d.cts +2 -2
  144. package/dist/plugins/open-api/api/index.d.mts +2 -2
  145. package/dist/plugins/open-api/api/index.d.ts +2 -2
  146. package/dist/plugins/route-docs/client/index.d.cts +1 -1
  147. package/dist/plugins/route-docs/client/index.d.mts +1 -1
  148. package/dist/plugins/route-docs/client/index.d.ts +1 -1
  149. package/dist/plugins/ui-builder/index.d.cts +1 -1
  150. package/dist/plugins/ui-builder/index.d.mts +1 -1
  151. package/dist/plugins/ui-builder/index.d.ts +1 -1
  152. package/dist/shared/{stack.BoA0xkJv.d.cts → stack.7n9Y_u7N.d.cts} +33 -7
  153. package/dist/shared/{stack.BoA0xkJv.d.mts → stack.7n9Y_u7N.d.mts} +33 -7
  154. package/dist/shared/{stack.BoA0xkJv.d.ts → stack.7n9Y_u7N.d.ts} +33 -7
  155. package/dist/shared/stack.B1EeBt1b.d.ts +297 -0
  156. package/dist/shared/stack.BIXEI6v_.d.mts +419 -0
  157. package/dist/shared/stack.BKfolAyK.d.ts +419 -0
  158. package/dist/shared/stack.BeSm90va.d.ts +289 -0
  159. package/dist/shared/stack.BpolpQpf.d.cts +445 -0
  160. package/dist/shared/stack.C5dtIncc.d.mts +293 -0
  161. package/dist/shared/stack.CIP6QS9l.d.ts +293 -0
  162. package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
  163. package/dist/shared/stack.CP68pFEH.d.mts +297 -0
  164. package/dist/shared/{stack.BsXokfNh.d.mts → stack.CVDTkMoO.d.cts} +8 -2
  165. package/dist/shared/{stack.BsXokfNh.d.ts → stack.CVDTkMoO.d.mts} +8 -2
  166. package/dist/shared/{stack.BsXokfNh.d.cts → stack.CVDTkMoO.d.ts} +8 -2
  167. package/dist/shared/{stack.DKDMI-QO.d.mts → stack.DJaKVY7v.d.cts} +7 -1
  168. package/dist/shared/{stack.DKDMI-QO.d.ts → stack.DJaKVY7v.d.mts} +7 -1
  169. package/dist/shared/{stack.DKDMI-QO.d.cts → stack.DJaKVY7v.d.ts} +7 -1
  170. package/dist/shared/{stack.DzH_wcvr.d.mts → stack.DdI5W6MB.d.cts} +9 -3
  171. package/dist/shared/{stack.DzH_wcvr.d.ts → stack.DdI5W6MB.d.mts} +9 -3
  172. package/dist/shared/{stack.DzH_wcvr.d.cts → stack.DdI5W6MB.d.ts} +9 -3
  173. package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
  174. package/dist/shared/stack.Dw0Ly2TM.d.cts +293 -0
  175. package/dist/shared/stack.IdtKDRka.d.cts +297 -0
  176. package/dist/shared/stack.TIBF2AOx.d.ts +445 -0
  177. package/dist/shared/stack.rTy7-wQU.d.mts +445 -0
  178. package/dist/shared/stack.snB1EDP7.d.cts +419 -0
  179. package/package.json +3 -3
  180. package/src/__tests__/stack-api.test.ts +118 -0
  181. package/src/api/index.ts +15 -1
  182. package/src/plugins/ai-chat/__tests__/getters.test.ts +109 -0
  183. package/src/plugins/ai-chat/api/getters.ts +71 -0
  184. package/src/plugins/ai-chat/api/index.ts +1 -0
  185. package/src/plugins/ai-chat/api/plugin.ts +8 -0
  186. package/src/plugins/api/index.ts +3 -1
  187. package/src/plugins/blog/__tests__/getters.test.ts +540 -0
  188. package/src/plugins/blog/api/getters.ts +243 -0
  189. package/src/plugins/blog/api/index.ts +9 -0
  190. package/src/plugins/blog/api/plugin.ts +98 -141
  191. package/src/plugins/blog/api/query-key-defs.ts +46 -0
  192. package/src/plugins/blog/api/serializers.ts +27 -0
  193. package/src/plugins/blog/client/plugin.tsx +21 -1
  194. package/src/plugins/blog/query-keys.ts +21 -20
  195. package/src/plugins/client/index.ts +1 -1
  196. package/src/plugins/cms/__tests__/getters.test.ts +206 -0
  197. package/src/plugins/cms/api/getters.ts +268 -0
  198. package/src/plugins/cms/api/index.ts +15 -1
  199. package/src/plugins/cms/api/plugin.ts +151 -150
  200. package/src/plugins/cms/api/query-key-defs.ts +53 -0
  201. package/src/plugins/cms/api/serializers.ts +12 -0
  202. package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +1 -1
  203. package/src/plugins/cms/client/hooks/cms-hooks.tsx +3 -0
  204. package/src/plugins/cms/client/plugin.tsx +19 -0
  205. package/src/plugins/cms/query-keys.ts +2 -1
  206. package/src/plugins/cms/types.ts +1 -1
  207. package/src/plugins/form-builder/__tests__/getters.test.ts +159 -0
  208. package/src/plugins/form-builder/api/getters.ts +226 -0
  209. package/src/plugins/form-builder/api/index.ts +15 -1
  210. package/src/plugins/form-builder/api/plugin.ts +107 -109
  211. package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
  212. package/src/plugins/form-builder/api/serializers.ts +12 -0
  213. package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
  214. package/src/plugins/form-builder/client/plugin.tsx +19 -0
  215. package/src/plugins/form-builder/query-keys.ts +6 -2
  216. package/src/plugins/form-builder/types.ts +2 -2
  217. package/src/plugins/kanban/__tests__/getters.test.ts +172 -0
  218. package/src/plugins/kanban/api/getters.ts +149 -0
  219. package/src/plugins/kanban/api/index.ts +4 -0
  220. package/src/plugins/kanban/api/plugin.ts +65 -146
  221. package/src/plugins/kanban/api/query-key-defs.ts +54 -0
  222. package/src/plugins/kanban/api/serializers.ts +49 -0
  223. package/src/plugins/kanban/client/plugin.tsx +15 -1
  224. package/src/plugins/kanban/query-keys.ts +10 -14
  225. package/src/plugins/utils.ts +19 -0
  226. package/src/types.ts +44 -5
  227. package/dist/shared/{stack.CbuN2zVV.d.cts → stack.CBON0dWL.d.cts} +7 -7
  228. package/dist/shared/{stack.CbuN2zVV.d.mts → stack.CBON0dWL.d.mts} +7 -7
  229. package/dist/shared/{stack.CbuN2zVV.d.ts → stack.CBON0dWL.d.ts} +7 -7
@@ -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
+ }
@@ -1,6 +1,10 @@
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";
8
+ export { getAllBoards, getBoardById, type BoardListResult } from "./getters";
9
+ export { serializeBoard, serializeColumn, serializeTask } from "./serializers";
10
+ export { KANBAN_QUERY_KEYS } from "./query-key-defs";
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  import { kanbanSchema as dbSchema } from "../db";
6
6
  import type {
7
7
  Board,
8
- BoardWithKanbanColumn,
8
+ BoardWithColumns,
9
9
  Column,
10
10
  ColumnWithTasks,
11
11
  Task,
@@ -23,6 +23,55 @@ import {
23
23
  updateColumnSchema,
24
24
  updateTaskSchema,
25
25
  } from "../schemas";
26
+ import { getAllBoards, getBoardById } from "./getters";
27
+ import { KANBAN_QUERY_KEYS } from "./query-key-defs";
28
+ import { serializeBoard } from "./serializers";
29
+ import type { QueryClient } from "@tanstack/react-query";
30
+
31
+ /**
32
+ * Route keys for the Kanban plugin — matches the keys returned by
33
+ * `stackClient.router.getRoute(path).routeKey`.
34
+ */
35
+ export type KanbanRouteKey = "boards" | "newBoard" | "board";
36
+
37
+ interface KanbanPrefetchForRoute {
38
+ (key: "boards" | "newBoard", qc: QueryClient): Promise<void>;
39
+ (key: "board", qc: QueryClient, params: { boardId: string }): Promise<void>;
40
+ }
41
+
42
+ function createKanbanPrefetchForRoute(
43
+ adapter: Adapter,
44
+ ): KanbanPrefetchForRoute {
45
+ return async function prefetchForRoute(
46
+ key: KanbanRouteKey,
47
+ qc: QueryClient,
48
+ params?: Record<string, string>,
49
+ ): Promise<void> {
50
+ switch (key) {
51
+ case "boards": {
52
+ const result = await getAllBoards(adapter, { limit: 50, offset: 0 });
53
+ qc.setQueryData(
54
+ KANBAN_QUERY_KEYS.boardsList({}),
55
+ result.items.map(serializeBoard),
56
+ );
57
+ break;
58
+ }
59
+ case "board": {
60
+ const boardId = params?.boardId ?? "";
61
+ if (boardId) {
62
+ const board = await getBoardById(adapter, boardId);
63
+ qc.setQueryData(
64
+ KANBAN_QUERY_KEYS.boardDetail(boardId),
65
+ board ? serializeBoard(board) : null,
66
+ );
67
+ }
68
+ break;
69
+ }
70
+ default:
71
+ break;
72
+ }
73
+ } as KanbanPrefetchForRoute;
74
+ }
26
75
 
27
76
  /**
28
77
  * Context passed to kanban API hooks
@@ -84,10 +133,12 @@ export interface KanbanBackendHooks {
84
133
  ) => Promise<boolean> | boolean;
85
134
 
86
135
  /**
87
- * Called after boards are listed successfully
136
+ * Called after boards are listed successfully.
137
+ * Receives the items array (same shape as `board[]`) for consistency
138
+ * with analogous hooks in other plugins (e.g. `onPostsRead`).
88
139
  */
89
140
  onBoardsRead?: (
90
- boards: Board[],
141
+ boards: BoardWithColumns[],
91
142
  filter: z.infer<typeof BoardListQuerySchema>,
92
143
  context: KanbanApiContext,
93
144
  ) => Promise<void> | void;
@@ -261,6 +312,13 @@ export const kanbanBackendPlugin = (hooks?: KanbanBackendHooks) =>
261
312
 
262
313
  dbPlugin: dbSchema,
263
314
 
315
+ api: (adapter) => ({
316
+ getAllBoards: (params?: Parameters<typeof getAllBoards>[1]) =>
317
+ getAllBoards(adapter, params),
318
+ getBoardById: (id: string) => getBoardById(adapter, id),
319
+ prefetchForRoute: createKanbanPrefetchForRoute(adapter),
320
+ }),
321
+
264
322
  routes: (adapter: Adapter) => {
265
323
  // ============ Board Endpoints ============
266
324
 
@@ -284,100 +342,10 @@ export const kanbanBackendPlugin = (hooks?: KanbanBackendHooks) =>
284
342
  }
285
343
  }
286
344
 
287
- const whereConditions = [];
288
-
289
- if (query.slug) {
290
- whereConditions.push({
291
- field: "slug",
292
- value: query.slug,
293
- operator: "eq" as const,
294
- });
295
- }
296
-
297
- if (query.ownerId) {
298
- whereConditions.push({
299
- field: "ownerId",
300
- value: query.ownerId,
301
- operator: "eq" as const,
302
- });
303
- }
304
-
305
- if (query.organizationId) {
306
- whereConditions.push({
307
- field: "organizationId",
308
- value: query.organizationId,
309
- operator: "eq" as const,
310
- });
311
- }
312
-
313
- const boards = await adapter.findMany<BoardWithKanbanColumn>({
314
- model: "kanbanBoard",
315
- limit: query.limit ?? 50,
316
- offset: query.offset ?? 0,
317
- where: whereConditions,
318
- sortBy: {
319
- field: "createdAt",
320
- direction: "desc",
321
- },
322
- join: {
323
- kanbanColumn: true,
324
- },
325
- });
326
-
327
- // Get all column IDs to fetch tasks
328
- // Note: adapter returns joined data under schema key name ("column"), not model name
329
- const columnIds: string[] = [];
330
- for (const board of boards) {
331
- if (board.column) {
332
- for (const col of board.column) {
333
- columnIds.push(col.id);
334
- }
335
- }
336
- }
337
-
338
- // Fetch tasks for each column in parallel (avoids loading all tasks from DB)
339
- const tasksByColumn = new Map<string, Task[]>();
340
- if (columnIds.length > 0) {
341
- const taskQueries = columnIds.map((columnId) =>
342
- adapter.findMany<Task>({
343
- model: "kanbanTask",
344
- where: [
345
- {
346
- field: "columnId",
347
- value: columnId,
348
- operator: "eq" as const,
349
- },
350
- ],
351
- sortBy: { field: "order", direction: "asc" },
352
- }),
353
- );
354
- const taskResults = await Promise.all(taskQueries);
355
- for (let i = 0; i < columnIds.length; i++) {
356
- const columnId = columnIds[i];
357
- const tasks = taskResults[i];
358
- if (columnId && tasks) {
359
- tasksByColumn.set(columnId, tasks);
360
- }
361
- }
362
- }
363
-
364
- // Map boards with columns and tasks
365
- const result = boards.map((board) => {
366
- const columns = (board.column || [])
367
- .sort((a, b) => a.order - b.order)
368
- .map((col) => ({
369
- ...col,
370
- tasks: tasksByColumn.get(col.id) || [],
371
- }));
372
- const { column: _, ...boardWithoutJoin } = board;
373
- return {
374
- ...boardWithoutJoin,
375
- columns,
376
- };
377
- });
345
+ const result = await getAllBoards(adapter, query);
378
346
 
379
347
  if (hooks?.onBoardsRead) {
380
- await hooks.onBoardsRead(result, query, context);
348
+ await hooks.onBoardsRead(result.items, query, context);
381
349
  }
382
350
 
383
351
  return result;
@@ -409,61 +377,12 @@ export const kanbanBackendPlugin = (hooks?: KanbanBackendHooks) =>
409
377
  }
410
378
  }
411
379
 
412
- const board = await adapter.findOne<BoardWithKanbanColumn>({
413
- model: "kanbanBoard",
414
- where: [
415
- { field: "id", value: params.id, operator: "eq" as const },
416
- ],
417
- join: {
418
- kanbanColumn: true,
419
- },
420
- });
380
+ const result = await getBoardById(adapter, params.id);
421
381
 
422
- if (!board) {
382
+ if (!result) {
423
383
  throw ctx.error(404, { message: "Board not found" });
424
384
  }
425
385
 
426
- // Fetch tasks for each column in parallel (avoids loading all tasks from DB)
427
- // Note: adapter returns joined data under schema key name ("column"), not model name
428
- const columnIds = (board.column || []).map((c) => c.id);
429
- const tasksByColumn = new Map<string, Task[]>();
430
- if (columnIds.length > 0) {
431
- const taskQueries = columnIds.map((columnId) =>
432
- adapter.findMany<Task>({
433
- model: "kanbanTask",
434
- where: [
435
- {
436
- field: "columnId",
437
- value: columnId,
438
- operator: "eq" as const,
439
- },
440
- ],
441
- sortBy: { field: "order", direction: "asc" },
442
- }),
443
- );
444
- const taskResults = await Promise.all(taskQueries);
445
- for (let i = 0; i < columnIds.length; i++) {
446
- const columnId = columnIds[i];
447
- const tasks = taskResults[i];
448
- if (columnId && tasks) {
449
- tasksByColumn.set(columnId, tasks);
450
- }
451
- }
452
- }
453
-
454
- const columns = (board.column || [])
455
- .sort((a, b) => a.order - b.order)
456
- .map((col) => ({
457
- ...col,
458
- tasks: tasksByColumn.get(col.id) || [],
459
- }));
460
-
461
- const { column: _, ...boardWithoutJoin } = board;
462
- const result = {
463
- ...boardWithoutJoin,
464
- columns,
465
- };
466
-
467
386
  if (hooks?.onBoardRead) {
468
387
  await hooks.onBoardRead(result, context);
469
388
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Internal query key constants for the Kanban 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 BoardsListDiscriminator {
8
+ slug: string | undefined;
9
+ ownerId: string | undefined;
10
+ organizationId: string | undefined;
11
+ limit: number;
12
+ offset: number;
13
+ }
14
+
15
+ /**
16
+ * Builds the discriminator object for the boards list query key.
17
+ * Mirrors the inline object used in createBoardsQueries.list.
18
+ */
19
+ export function boardsListDiscriminator(params?: {
20
+ slug?: string;
21
+ ownerId?: string;
22
+ organizationId?: string;
23
+ limit?: number;
24
+ offset?: number;
25
+ }): BoardsListDiscriminator {
26
+ return {
27
+ slug: params?.slug,
28
+ ownerId: params?.ownerId,
29
+ organizationId: params?.organizationId,
30
+ limit: params?.limit ?? 50,
31
+ offset: params?.offset ?? 0,
32
+ };
33
+ }
34
+
35
+ /** Full query key builders — use these with queryClient.setQueryData() */
36
+ export const KANBAN_QUERY_KEYS = {
37
+ /**
38
+ * Key for boards.list(params) query.
39
+ * Full key: ["boards", "list", { slug, ownerId, organizationId, limit, offset }]
40
+ */
41
+ boardsList: (params?: {
42
+ slug?: string;
43
+ ownerId?: string;
44
+ organizationId?: string;
45
+ limit?: number;
46
+ offset?: number;
47
+ }) => ["boards", "list", boardsListDiscriminator(params)] as const,
48
+
49
+ /**
50
+ * Key for boards.detail(boardId) query.
51
+ * Full key: ["boards", "detail", boardId]
52
+ */
53
+ boardDetail: (boardId: string) => ["boards", "detail", boardId] as const,
54
+ };
@@ -0,0 +1,49 @@
1
+ import type {
2
+ Task,
3
+ ColumnWithTasks,
4
+ BoardWithColumns,
5
+ SerializedTask,
6
+ SerializedColumn,
7
+ SerializedBoardWithColumns,
8
+ } from "../types";
9
+
10
+ /**
11
+ * Serialize a Task for SSR/SSG use (convert dates to strings).
12
+ * Pure function — no DB access, no hooks.
13
+ */
14
+ export function serializeTask(task: Task): SerializedTask {
15
+ return {
16
+ ...task,
17
+ completedAt: task.completedAt?.toISOString(),
18
+ createdAt: task.createdAt.toISOString(),
19
+ updatedAt: task.updatedAt.toISOString(),
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Serialize a Column (with its tasks) for SSR/SSG use (convert dates to strings).
25
+ * Pure function — no DB access, no hooks.
26
+ */
27
+ export function serializeColumn(col: ColumnWithTasks): SerializedColumn {
28
+ return {
29
+ ...col,
30
+ createdAt: col.createdAt.toISOString(),
31
+ updatedAt: col.updatedAt.toISOString(),
32
+ tasks: col.tasks.map(serializeTask),
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Serialize a Board (with columns and tasks) for SSR/SSG use (convert dates to strings).
38
+ * Pure function — no DB access, no hooks.
39
+ */
40
+ export function serializeBoard(
41
+ board: BoardWithColumns,
42
+ ): SerializedBoardWithColumns {
43
+ return {
44
+ ...board,
45
+ createdAt: board.createdAt.toISOString(),
46
+ updatedAt: board.updatedAt.toISOString(),
47
+ columns: board.columns.map(serializeColumn),
48
+ };
49
+ }