@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
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { stack } from "../api";
3
+ import { defineBackendPlugin } from "../plugins/api";
4
+ import { createDbPlugin } from "@btst/db";
5
+ import { createMemoryAdapter } from "@btst/adapter-memory";
6
+ import type { Adapter, DatabaseDefinition } from "@btst/db";
7
+ import { blogBackendPlugin } from "../plugins/blog/api";
8
+ import { kanbanBackendPlugin } from "../plugins/kanban/api";
9
+
10
+ const testAdapter = (db: DatabaseDefinition): Adapter =>
11
+ createMemoryAdapter(db)({});
12
+
13
+ /**
14
+ * A minimal plugin with no `api` factory, to verify backward compatibility.
15
+ */
16
+ const noApiPlugin = defineBackendPlugin({
17
+ name: "no-api",
18
+ dbPlugin: createDbPlugin("no-api", {}),
19
+ routes: () => ({}),
20
+ });
21
+
22
+ describe("stack.api surface", () => {
23
+ it("exposes adapter on the returned backend", () => {
24
+ const backend = stack({
25
+ basePath: "/api",
26
+ plugins: { blog: blogBackendPlugin() },
27
+ adapter: testAdapter,
28
+ });
29
+
30
+ expect(backend.adapter).toBeDefined();
31
+ expect(typeof backend.adapter.findMany).toBe("function");
32
+ expect(typeof backend.adapter.findOne).toBe("function");
33
+ expect(typeof backend.adapter.create).toBe("function");
34
+ });
35
+
36
+ it("exposes typed api namespace for plugins with api factory", () => {
37
+ const backend = stack({
38
+ basePath: "/api",
39
+ plugins: { blog: blogBackendPlugin() },
40
+ adapter: testAdapter,
41
+ });
42
+
43
+ expect(backend.api).toBeDefined();
44
+ expect(backend.api.blog).toBeDefined();
45
+ expect(typeof backend.api.blog.getAllPosts).toBe("function");
46
+ expect(typeof backend.api.blog.getPostBySlug).toBe("function");
47
+ expect(typeof backend.api.blog.getAllTags).toBe("function");
48
+ });
49
+
50
+ it("exposes kanban api namespace", () => {
51
+ const backend = stack({
52
+ basePath: "/api",
53
+ plugins: { kanban: kanbanBackendPlugin() },
54
+ adapter: testAdapter,
55
+ });
56
+
57
+ expect(backend.api.kanban).toBeDefined();
58
+ expect(typeof backend.api.kanban.getAllBoards).toBe("function");
59
+ expect(typeof backend.api.kanban.getBoardById).toBe("function");
60
+ });
61
+
62
+ it("plugins without api factory are not present in api", () => {
63
+ const backend = stack({
64
+ basePath: "/api",
65
+ plugins: { noApi: noApiPlugin },
66
+ adapter: testAdapter,
67
+ });
68
+
69
+ expect((backend.api as any).noApi).toBeUndefined();
70
+ });
71
+
72
+ it("api functions are bound to the shared adapter and return real data", async () => {
73
+ const backend = stack({
74
+ basePath: "/api",
75
+ plugins: { blog: blogBackendPlugin() },
76
+ adapter: testAdapter,
77
+ });
78
+
79
+ // Seed data via adapter directly
80
+ await backend.adapter.create({
81
+ model: "post",
82
+ data: {
83
+ title: "Hello World",
84
+ slug: "hello-world",
85
+ content: "Content",
86
+ excerpt: "",
87
+ published: true,
88
+ tags: [],
89
+ createdAt: new Date(),
90
+ updatedAt: new Date(),
91
+ },
92
+ });
93
+
94
+ // Retrieve via stack.api
95
+ const posts = await backend.api.blog.getAllPosts();
96
+ expect(posts.items).toHaveLength(1);
97
+ expect(posts.items[0]!.slug).toBe("hello-world");
98
+
99
+ // Verify same adapter - data is shared
100
+ const bySlug = await backend.api.blog.getPostBySlug("hello-world");
101
+ expect(bySlug).not.toBeNull();
102
+ expect(bySlug!.title).toBe("Hello World");
103
+ });
104
+
105
+ it("combines multiple plugins in a single stack call", () => {
106
+ const backend = stack({
107
+ basePath: "/api",
108
+ plugins: {
109
+ blog: blogBackendPlugin(),
110
+ kanban: kanbanBackendPlugin(),
111
+ },
112
+ adapter: testAdapter,
113
+ });
114
+
115
+ expect(typeof backend.api.blog.getAllPosts).toBe("function");
116
+ expect(typeof backend.api.kanban.getAllBoards).toBe("function");
117
+ });
118
+ });
package/src/api/index.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  BackendLibConfig,
4
4
  BackendLib,
5
5
  PrefixedPluginRoutes,
6
+ PluginApis,
6
7
  StackContext,
7
8
  } from "../types";
8
9
  import { defineDb } from "@btst/db";
@@ -33,7 +34,9 @@ export function stack<
33
34
  TPlugins extends Record<string, any>,
34
35
  TRoutes extends
35
36
  PrefixedPluginRoutes<TPlugins> = PrefixedPluginRoutes<TPlugins>,
36
- >(config: BackendLibConfig<TPlugins>): BackendLib<TRoutes> {
37
+ >(
38
+ config: BackendLibConfig<TPlugins>,
39
+ ): BackendLib<TRoutes, PluginApis<TPlugins>> {
37
40
  const { plugins, adapter, dbSchema, basePath } = config;
38
41
 
39
42
  // Collect all routes from all plugins with type-safe prefixed keys
@@ -67,6 +70,14 @@ export function stack<
67
70
  }
68
71
  }
69
72
 
73
+ // Build the typed api surface by calling each plugin's api factory
74
+ const pluginApis = {} as PluginApis<TPlugins>;
75
+ for (const [pluginKey, plugin] of Object.entries(plugins)) {
76
+ if (plugin.api) {
77
+ (pluginApis as any)[pluginKey] = plugin.api(adapterInstance);
78
+ }
79
+ }
80
+
70
81
  // Create the composed router
71
82
  const router = createRouter(allRoutes, {
72
83
  basePath: basePath,
@@ -76,6 +87,8 @@ export function stack<
76
87
  handler: router.handler,
77
88
  router,
78
89
  dbSchema: betterDbSchema,
90
+ adapter: adapterInstance,
91
+ api: pluginApis,
79
92
  };
80
93
  }
81
94
 
@@ -83,5 +96,6 @@ export type {
83
96
  BackendPlugin,
84
97
  BackendLibConfig,
85
98
  BackendLib,
99
+ PluginApis,
86
100
  StackContext,
87
101
  } from "../types";
@@ -0,0 +1,109 @@
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 { aiChatSchema } from "../db";
6
+ import { getAllConversations, getConversationById } from "../api/getters";
7
+
8
+ const createTestAdapter = (): Adapter => {
9
+ const db = defineDb({}).use(aiChatSchema);
10
+ return createMemoryAdapter(db)({});
11
+ };
12
+
13
+ async function createConversation(
14
+ adapter: Adapter,
15
+ title: string,
16
+ userId?: string,
17
+ ): Promise<any> {
18
+ return adapter.create({
19
+ model: "conversation",
20
+ data: {
21
+ title,
22
+ ...(userId ? { userId } : {}),
23
+ createdAt: new Date(),
24
+ updatedAt: new Date(),
25
+ },
26
+ });
27
+ }
28
+
29
+ describe("ai-chat getters", () => {
30
+ let adapter: Adapter;
31
+
32
+ beforeEach(() => {
33
+ adapter = createTestAdapter();
34
+ });
35
+
36
+ describe("getAllConversations", () => {
37
+ it("returns empty array when no conversations exist", async () => {
38
+ const convs = await getAllConversations(adapter);
39
+ expect(convs).toEqual([]);
40
+ });
41
+
42
+ it("returns all conversations sorted by updatedAt desc", async () => {
43
+ await createConversation(adapter, "First");
44
+ await createConversation(adapter, "Second");
45
+
46
+ const convs = await getAllConversations(adapter);
47
+ expect(convs).toHaveLength(2);
48
+ });
49
+
50
+ it("filters conversations by userId", async () => {
51
+ await createConversation(adapter, "Alice Conv", "user-alice");
52
+ await createConversation(adapter, "Bob Conv", "user-bob");
53
+ await createConversation(adapter, "No User Conv");
54
+
55
+ const aliceConvs = await getAllConversations(adapter, "user-alice");
56
+ expect(aliceConvs).toHaveLength(1);
57
+ expect(aliceConvs[0]!.title).toBe("Alice Conv");
58
+
59
+ const allConvs = await getAllConversations(adapter);
60
+ expect(allConvs).toHaveLength(3);
61
+ });
62
+ });
63
+
64
+ describe("getConversationById", () => {
65
+ it("returns null when conversation does not exist", async () => {
66
+ const conv = await getConversationById(adapter, "nonexistent");
67
+ expect(conv).toBeNull();
68
+ });
69
+
70
+ it("returns conversation with messages", async () => {
71
+ const conv = (await createConversation(adapter, "My Chat")) as any;
72
+
73
+ await adapter.create({
74
+ model: "message",
75
+ data: {
76
+ conversationId: conv.id,
77
+ role: "user",
78
+ content: JSON.stringify([{ type: "text", text: "Hello!" }]),
79
+ createdAt: new Date(Date.now() - 1000),
80
+ },
81
+ });
82
+ await adapter.create({
83
+ model: "message",
84
+ data: {
85
+ conversationId: conv.id,
86
+ role: "assistant",
87
+ content: JSON.stringify([{ type: "text", text: "Hi there!" }]),
88
+ createdAt: new Date(),
89
+ },
90
+ });
91
+
92
+ const result = await getConversationById(adapter, conv.id);
93
+ expect(result).not.toBeNull();
94
+ expect(result!.id).toBe(conv.id);
95
+ expect(result!.title).toBe("My Chat");
96
+ expect(result!.messages).toHaveLength(2);
97
+ expect(result!.messages[0]!.role).toBe("user");
98
+ expect(result!.messages[1]!.role).toBe("assistant");
99
+ });
100
+
101
+ it("returns conversation with empty messages array if none exist", async () => {
102
+ const conv = (await createConversation(adapter, "Empty Chat")) as any;
103
+
104
+ const result = await getConversationById(adapter, conv.id);
105
+ expect(result).not.toBeNull();
106
+ expect(result!.messages).toEqual([]);
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,71 @@
1
+ import type { Adapter } from "@btst/db";
2
+ import type { Conversation, ConversationWithMessages, Message } from "../types";
3
+
4
+ /**
5
+ * Retrieve all conversations, optionally filtered by userId.
6
+ * Pure DB function - no hooks, no HTTP context. Safe for server-side use.
7
+ *
8
+ * @param adapter - The database adapter
9
+ * @param userId - Optional user ID to filter conversations by owner
10
+ */
11
+ export async function getAllConversations(
12
+ adapter: Adapter,
13
+ userId?: string,
14
+ ): Promise<Conversation[]> {
15
+ const whereConditions: Array<{
16
+ field: string;
17
+ value: string;
18
+ operator: "eq";
19
+ }> = [];
20
+
21
+ if (userId) {
22
+ whereConditions.push({
23
+ field: "userId",
24
+ value: userId,
25
+ operator: "eq" as const,
26
+ });
27
+ }
28
+
29
+ return adapter.findMany<Conversation>({
30
+ model: "conversation",
31
+ where: whereConditions.length > 0 ? whereConditions : undefined,
32
+ sortBy: { field: "updatedAt", direction: "desc" },
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Retrieve a single conversation by its ID, including all messages.
38
+ * Returns null if the conversation is not found.
39
+ * Pure DB function - no hooks, no HTTP context. Safe for server-side use.
40
+ *
41
+ * @param adapter - The database adapter
42
+ * @param id - The conversation ID
43
+ */
44
+ export async function getConversationById(
45
+ adapter: Adapter,
46
+ id: string,
47
+ ): Promise<(Conversation & { messages: Message[] }) | null> {
48
+ const conversations = await adapter.findMany<ConversationWithMessages>({
49
+ model: "conversation",
50
+ where: [{ field: "id", value: id, operator: "eq" as const }],
51
+ limit: 1,
52
+ join: {
53
+ message: true,
54
+ },
55
+ });
56
+
57
+ if (!conversations.length) {
58
+ return null;
59
+ }
60
+
61
+ const conversation = conversations[0]!;
62
+ const messages = (conversation.message || []).sort(
63
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
64
+ );
65
+
66
+ const { message: _, ...conversationWithoutJoin } = conversation;
67
+ return {
68
+ ...conversationWithoutJoin,
69
+ messages,
70
+ };
71
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./plugin";
2
+ export { getAllConversations, getConversationById } from "./getters";
2
3
  export { createAiChatQueryKeys } from "../query-keys";
@@ -16,6 +16,7 @@ import {
16
16
  updateConversationSchema,
17
17
  } from "../schemas";
18
18
  import type { Conversation, ConversationWithMessages, Message } from "../types";
19
+ import { getAllConversations, getConversationById } from "./getters";
19
20
 
20
21
  /**
21
22
  * Context passed to AI Chat API hooks
@@ -286,6 +287,13 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
286
287
  name: "ai-chat",
287
288
  // Always include db schema - in public mode we just don't use it
288
289
  dbPlugin: dbSchema,
290
+
291
+ api: (adapter) => ({
292
+ getAllConversations: (userId?: string) =>
293
+ getAllConversations(adapter, userId),
294
+ getConversationById: (id: string) => getConversationById(adapter, id),
295
+ }),
296
+
289
297
  routes: (adapter: Adapter) => {
290
298
  const mode = config.mode ?? "authenticated";
291
299
  const isPublicMode = mode === "public";
@@ -42,9 +42,11 @@ export { createDbPlugin } from "@btst/db";
42
42
  * ```
43
43
  *
44
44
  * @template TRoutes - The exact shape of routes (auto-inferred from routes function)
45
+ * @template TApi - The shape of the server-side api surface (auto-inferred from api factory)
45
46
  */
46
47
  export function defineBackendPlugin<
47
48
  TRoutes extends Record<string, Endpoint> = Record<string, Endpoint>,
48
- >(plugin: BackendPlugin<TRoutes>): BackendPlugin<TRoutes> {
49
+ TApi extends Record<string, (...args: any[]) => any> = never,
50
+ >(plugin: BackendPlugin<TRoutes, TApi>): BackendPlugin<TRoutes, TApi> {
49
51
  return plugin;
50
52
  }