@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
|
@@ -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
|
-
>(
|
|
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
|
+
}
|
|
@@ -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";
|
package/src/plugins/api/index.ts
CHANGED
|
@@ -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
|
-
|
|
49
|
+
TApi extends Record<string, (...args: any[]) => any> = never,
|
|
50
|
+
>(plugin: BackendPlugin<TRoutes, TApi>): BackendPlugin<TRoutes, TApi> {
|
|
49
51
|
return plugin;
|
|
50
52
|
}
|