@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.
- 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 +60 -107
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +60 -107
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +16 -1
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +17 -2
- package/dist/packages/stack/src/plugins/cms/api/getters.cjs +156 -0
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +147 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +624 -617
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +623 -616
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
- 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/cms/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +120 -0
- package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +112 -0
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +75 -86
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +71 -82
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
- 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/form-builder/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -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 +37 -123
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +37 -123
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +11 -1
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +12 -2
- package/dist/packages/stack/src/plugins/utils.cjs +6 -0
- package/dist/packages/stack/src/plugins/utils.mjs +6 -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 +9 -0
- package/dist/plugins/blog/api/index.d.cts +20 -4
- package/dist/plugins/blog/api/index.d.mts +20 -4
- package/dist/plugins/blog/api/index.d.ts +20 -4
- package/dist/plugins/blog/api/index.mjs +3 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +5 -5
- package/dist/plugins/blog/client/hooks/index.d.mts +5 -5
- package/dist/plugins/blog/client/hooks/index.d.ts +5 -5
- 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 +13 -9
- package/dist/plugins/blog/query-keys.d.cts +8 -333
- package/dist/plugins/blog/query-keys.d.mts +8 -333
- package/dist/plugins/blog/query-keys.d.ts +8 -333
- package/dist/plugins/blog/query-keys.mjs +13 -9
- package/dist/plugins/client/index.cjs +1 -0
- package/dist/plugins/client/index.d.cts +10 -3
- package/dist/plugins/client/index.d.mts +10 -3
- package/dist/plugins/client/index.d.ts +10 -3
- package/dist/plugins/client/index.mjs +1 -1
- package/dist/plugins/cms/api/index.cjs +10 -0
- package/dist/plugins/cms/api/index.d.cts +7 -163
- package/dist/plugins/cms/api/index.d.mts +7 -163
- package/dist/plugins/cms/api/index.d.ts +7 -163
- package/dist/plugins/cms/api/index.mjs +2 -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.cjs +2 -1
- package/dist/plugins/cms/query-keys.d.cts +6 -9
- package/dist/plugins/cms/query-keys.d.mts +6 -9
- package/dist/plugins/cms/query-keys.d.ts +6 -9
- package/dist/plugins/cms/query-keys.mjs +2 -1
- package/dist/plugins/form-builder/api/index.cjs +10 -0
- package/dist/plugins/form-builder/api/index.d.cts +7 -141
- package/dist/plugins/form-builder/api/index.d.mts +7 -141
- package/dist/plugins/form-builder/api/index.d.ts +7 -141
- package/dist/plugins/form-builder/api/index.mjs +2 -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.cjs +3 -2
- package/dist/plugins/form-builder/query-keys.d.cts +7 -6
- package/dist/plugins/form-builder/query-keys.d.mts +7 -6
- package/dist/plugins/form-builder/query-keys.d.ts +7 -6
- package/dist/plugins/form-builder/query-keys.mjs +3 -2
- package/dist/plugins/kanban/api/index.cjs +9 -0
- package/dist/plugins/kanban/api/index.d.cts +17 -395
- package/dist/plugins/kanban/api/index.d.mts +17 -395
- package/dist/plugins/kanban/api/index.d.ts +17 -395
- package/dist/plugins/kanban/api/index.mjs +3 -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 +6 -12
- package/dist/plugins/kanban/query-keys.d.cts +5 -16
- package/dist/plugins/kanban/query-keys.d.mts +5 -16
- package/dist/plugins/kanban/query-keys.d.ts +5 -16
- package/dist/plugins/kanban/query-keys.mjs +6 -12
- 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.B1EeBt1b.d.ts +297 -0
- package/dist/shared/stack.BIXEI6v_.d.mts +419 -0
- package/dist/shared/stack.BKfolAyK.d.ts +419 -0
- package/dist/shared/stack.BeSm90va.d.ts +289 -0
- package/dist/shared/stack.BpolpQpf.d.cts +445 -0
- package/dist/shared/stack.C5dtIncc.d.mts +293 -0
- package/dist/shared/stack.CIP6QS9l.d.ts +293 -0
- package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
- package/dist/shared/stack.CP68pFEH.d.mts +297 -0
- package/dist/shared/{stack.BsXokfNh.d.mts → stack.CVDTkMoO.d.cts} +8 -2
- package/dist/shared/{stack.BsXokfNh.d.ts → stack.CVDTkMoO.d.mts} +8 -2
- package/dist/shared/{stack.BsXokfNh.d.cts → stack.CVDTkMoO.d.ts} +8 -2
- package/dist/shared/{stack.DKDMI-QO.d.mts → stack.DJaKVY7v.d.cts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.ts → stack.DJaKVY7v.d.mts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.cts → stack.DJaKVY7v.d.ts} +7 -1
- package/dist/shared/{stack.DzH_wcvr.d.mts → stack.DdI5W6MB.d.cts} +9 -3
- package/dist/shared/{stack.DzH_wcvr.d.ts → stack.DdI5W6MB.d.mts} +9 -3
- package/dist/shared/{stack.DzH_wcvr.d.cts → stack.DdI5W6MB.d.ts} +9 -3
- package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
- package/dist/shared/stack.Dw0Ly2TM.d.cts +293 -0
- package/dist/shared/stack.IdtKDRka.d.cts +297 -0
- package/dist/shared/stack.TIBF2AOx.d.ts +445 -0
- package/dist/shared/stack.rTy7-wQU.d.mts +445 -0
- package/dist/shared/stack.snB1EDP7.d.cts +419 -0
- package/package.json +3 -3
- 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 +9 -0
- package/src/plugins/blog/api/plugin.ts +98 -141
- package/src/plugins/blog/api/query-key-defs.ts +46 -0
- package/src/plugins/blog/api/serializers.ts +27 -0
- package/src/plugins/blog/client/plugin.tsx +21 -1
- package/src/plugins/blog/query-keys.ts +21 -20
- package/src/plugins/client/index.ts +1 -1
- package/src/plugins/cms/__tests__/getters.test.ts +206 -0
- package/src/plugins/cms/api/getters.ts +268 -0
- package/src/plugins/cms/api/index.ts +15 -1
- package/src/plugins/cms/api/plugin.ts +151 -150
- package/src/plugins/cms/api/query-key-defs.ts +53 -0
- package/src/plugins/cms/api/serializers.ts +12 -0
- 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/client/plugin.tsx +19 -0
- package/src/plugins/cms/query-keys.ts +2 -1
- 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 +226 -0
- package/src/plugins/form-builder/api/index.ts +15 -1
- package/src/plugins/form-builder/api/plugin.ts +107 -109
- package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
- package/src/plugins/form-builder/api/serializers.ts +12 -0
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
- package/src/plugins/form-builder/client/plugin.tsx +19 -0
- package/src/plugins/form-builder/query-keys.ts +6 -2
- 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 +4 -0
- package/src/plugins/kanban/api/plugin.ts +65 -146
- package/src/plugins/kanban/api/query-key-defs.ts +54 -0
- package/src/plugins/kanban/api/serializers.ts +49 -0
- package/src/plugins/kanban/client/plugin.tsx +15 -1
- package/src/plugins/kanban/query-keys.ts +10 -14
- package/src/plugins/utils.ts +19 -0
- package/src/types.ts +44 -5
- package/dist/shared/{stack.CbuN2zVV.d.cts → stack.CBON0dWL.d.cts} +7 -7
- package/dist/shared/{stack.CbuN2zVV.d.mts → stack.CBON0dWL.d.mts} +7 -7
- package/dist/shared/{stack.CbuN2zVV.d.ts → stack.CBON0dWL.d.ts} +7 -7
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import type { BlogApiRouter } from "./api";
|
|
6
6
|
import { createApiClient } from "@btst/stack/plugins/client";
|
|
7
7
|
import type { SerializedPost, SerializedTag } from "./types";
|
|
8
|
+
import { postsListDiscriminator } from "./api/query-key-defs";
|
|
8
9
|
|
|
9
10
|
interface PostsListParams {
|
|
10
11
|
query?: string;
|
|
@@ -71,15 +72,12 @@ function createPostsQueries(
|
|
|
71
72
|
return createQueryKeys("posts", {
|
|
72
73
|
list: (params?: PostsListParams) => ({
|
|
73
74
|
queryKey: [
|
|
74
|
-
{
|
|
75
|
-
query:
|
|
76
|
-
params?.query !== undefined && params?.query?.trim() === ""
|
|
77
|
-
? undefined
|
|
78
|
-
: params?.query,
|
|
79
|
-
limit: params?.limit ?? 10,
|
|
75
|
+
postsListDiscriminator({
|
|
80
76
|
published: params?.published ?? true,
|
|
77
|
+
limit: params?.limit ?? 10,
|
|
81
78
|
tagSlug: params?.tagSlug,
|
|
82
|
-
|
|
79
|
+
query: params?.query,
|
|
80
|
+
}),
|
|
83
81
|
],
|
|
84
82
|
queryFn: async ({ pageParam }: { pageParam?: number }) => {
|
|
85
83
|
try {
|
|
@@ -99,13 +97,14 @@ function createPostsQueries(
|
|
|
99
97
|
},
|
|
100
98
|
headers,
|
|
101
99
|
});
|
|
102
|
-
// Check for errors (better-call returns Error$1<unknown> | Data<
|
|
100
|
+
// Check for errors (better-call returns Error$1<unknown> | Data<PostListResult>)
|
|
103
101
|
if (isErrorResponse(response)) {
|
|
104
102
|
const errorResponse = response as { error: unknown };
|
|
105
103
|
throw toError(errorResponse.error);
|
|
106
104
|
}
|
|
107
|
-
//
|
|
108
|
-
|
|
105
|
+
// Extract .items from the paginated response for infinite scroll compatibility
|
|
106
|
+
const dataResponse = response as { data?: { items?: unknown[] } };
|
|
107
|
+
return (dataResponse.data?.items ??
|
|
109
108
|
[]) as unknown as SerializedPost[];
|
|
110
109
|
} catch (error) {
|
|
111
110
|
// Re-throw errors so React Query can catch them
|
|
@@ -126,14 +125,14 @@ function createPostsQueries(
|
|
|
126
125
|
query: { slug, limit: 1 },
|
|
127
126
|
headers,
|
|
128
127
|
});
|
|
129
|
-
// Check for errors (better-call returns Error$1<unknown> | Data<
|
|
128
|
+
// Check for errors (better-call returns Error$1<unknown> | Data<PostListResult>)
|
|
130
129
|
if (isErrorResponse(response)) {
|
|
131
130
|
const errorResponse = response as { error: unknown };
|
|
132
131
|
throw toError(errorResponse.error);
|
|
133
132
|
}
|
|
134
|
-
// Type narrowed to Data<
|
|
135
|
-
const dataResponse = response as { data?: unknown[] };
|
|
136
|
-
return (dataResponse.data?.[0] ??
|
|
133
|
+
// Type narrowed to Data<PostListResult> after error check — access .items[0]
|
|
134
|
+
const dataResponse = response as { data?: { items?: unknown[] } };
|
|
135
|
+
return (dataResponse.data?.items?.[0] ??
|
|
137
136
|
null) as unknown as SerializedPost | null;
|
|
138
137
|
} catch (error) {
|
|
139
138
|
// Re-throw errors so React Query can catch them
|
|
@@ -181,13 +180,14 @@ function createPostsQueries(
|
|
|
181
180
|
},
|
|
182
181
|
headers,
|
|
183
182
|
});
|
|
184
|
-
// Check for errors (better-call returns Error$1<unknown> | Data<
|
|
183
|
+
// Check for errors (better-call returns Error$1<unknown> | Data<PostListResult>)
|
|
185
184
|
if (isErrorResponse(response)) {
|
|
186
185
|
const errorResponse = response as { error: unknown };
|
|
187
186
|
throw toError(errorResponse.error);
|
|
188
187
|
}
|
|
189
|
-
//
|
|
190
|
-
|
|
188
|
+
// Extract .items from the paginated response
|
|
189
|
+
const recentResponse = response as { data?: { items?: unknown[] } };
|
|
190
|
+
let posts = (recentResponse.data?.items ??
|
|
191
191
|
[]) as unknown as SerializedPost[];
|
|
192
192
|
|
|
193
193
|
// Exclude current post if specified
|
|
@@ -228,13 +228,14 @@ function createDraftsQueries(
|
|
|
228
228
|
},
|
|
229
229
|
headers,
|
|
230
230
|
});
|
|
231
|
-
// Check for errors (better-call returns Error$1<unknown> | Data<
|
|
231
|
+
// Check for errors (better-call returns Error$1<unknown> | Data<PostListResult>)
|
|
232
232
|
if (isErrorResponse(response)) {
|
|
233
233
|
const errorResponse = response as { error: unknown };
|
|
234
234
|
throw toError(errorResponse.error);
|
|
235
235
|
}
|
|
236
|
-
//
|
|
237
|
-
|
|
236
|
+
// Extract .items from the paginated response for infinite scroll compatibility
|
|
237
|
+
const draftsResponse = response as { data?: { items?: unknown[] } };
|
|
238
|
+
return (draftsResponse.data?.items ??
|
|
238
239
|
[]) as unknown as SerializedPost[];
|
|
239
240
|
} catch (error) {
|
|
240
241
|
// Re-throw errors so React Query can catch them
|
|
@@ -18,7 +18,7 @@ export type {
|
|
|
18
18
|
PluginOverrides,
|
|
19
19
|
} from "../../types";
|
|
20
20
|
|
|
21
|
-
export { createApiClient } from "../utils";
|
|
21
|
+
export { createApiClient, isConnectionError } from "../utils";
|
|
22
22
|
|
|
23
23
|
// Re-export Yar types needed for plugins
|
|
24
24
|
export type { Route } from "@btst/yar";
|
|
@@ -0,0 +1,206 @@
|
|
|
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 { cmsSchema } from "../db";
|
|
6
|
+
import {
|
|
7
|
+
getAllContentTypes,
|
|
8
|
+
getAllContentItems,
|
|
9
|
+
getContentItemBySlug,
|
|
10
|
+
} from "../api/getters";
|
|
11
|
+
|
|
12
|
+
const createTestAdapter = (): Adapter => {
|
|
13
|
+
const db = defineDb({}).use(cmsSchema);
|
|
14
|
+
return createMemoryAdapter(db)({});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const SIMPLE_SCHEMA = JSON.stringify({
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
title: { type: "string" },
|
|
21
|
+
},
|
|
22
|
+
autoFormVersion: 2,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("cms getters", () => {
|
|
26
|
+
let adapter: Adapter;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
adapter = createTestAdapter();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("getAllContentTypes", () => {
|
|
33
|
+
it("returns empty array when no content types exist", async () => {
|
|
34
|
+
const types = await getAllContentTypes(adapter);
|
|
35
|
+
expect(types).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns serialized content types sorted by name", async () => {
|
|
39
|
+
await adapter.create({
|
|
40
|
+
model: "contentType",
|
|
41
|
+
data: {
|
|
42
|
+
name: "Post",
|
|
43
|
+
slug: "post",
|
|
44
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
45
|
+
autoFormVersion: 2,
|
|
46
|
+
createdAt: new Date(),
|
|
47
|
+
updatedAt: new Date(),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
await adapter.create({
|
|
51
|
+
model: "contentType",
|
|
52
|
+
data: {
|
|
53
|
+
name: "Article",
|
|
54
|
+
slug: "article",
|
|
55
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
56
|
+
autoFormVersion: 2,
|
|
57
|
+
createdAt: new Date(),
|
|
58
|
+
updatedAt: new Date(),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const types = await getAllContentTypes(adapter);
|
|
63
|
+
expect(types).toHaveLength(2);
|
|
64
|
+
// Sorted by name
|
|
65
|
+
expect(types[0]!.slug).toBe("article");
|
|
66
|
+
expect(types[1]!.slug).toBe("post");
|
|
67
|
+
// Dates are serialized as strings
|
|
68
|
+
expect(typeof types[0]!.createdAt).toBe("string");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("getAllContentItems", () => {
|
|
73
|
+
it("returns empty result when content type does not exist", async () => {
|
|
74
|
+
const result = await getAllContentItems(adapter, "nonexistent");
|
|
75
|
+
expect(result.items).toEqual([]);
|
|
76
|
+
expect(result.total).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns items for a content type", async () => {
|
|
80
|
+
const ct = (await adapter.create({
|
|
81
|
+
model: "contentType",
|
|
82
|
+
data: {
|
|
83
|
+
name: "Post",
|
|
84
|
+
slug: "post",
|
|
85
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
86
|
+
autoFormVersion: 2,
|
|
87
|
+
createdAt: new Date(),
|
|
88
|
+
updatedAt: new Date(),
|
|
89
|
+
},
|
|
90
|
+
})) as any;
|
|
91
|
+
|
|
92
|
+
await adapter.create({
|
|
93
|
+
model: "contentItem",
|
|
94
|
+
data: {
|
|
95
|
+
contentTypeId: ct.id,
|
|
96
|
+
slug: "my-post",
|
|
97
|
+
data: JSON.stringify({ title: "My Post" }),
|
|
98
|
+
createdAt: new Date(),
|
|
99
|
+
updatedAt: new Date(),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await getAllContentItems(adapter, "post");
|
|
104
|
+
expect(result.items).toHaveLength(1);
|
|
105
|
+
expect(result.total).toBe(1);
|
|
106
|
+
expect(result.items[0]!.slug).toBe("my-post");
|
|
107
|
+
expect(result.items[0]!.parsedData).toEqual({ title: "My Post" });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("filters items by slug", async () => {
|
|
111
|
+
const ct = (await adapter.create({
|
|
112
|
+
model: "contentType",
|
|
113
|
+
data: {
|
|
114
|
+
name: "Post",
|
|
115
|
+
slug: "post",
|
|
116
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
117
|
+
autoFormVersion: 2,
|
|
118
|
+
createdAt: new Date(),
|
|
119
|
+
updatedAt: new Date(),
|
|
120
|
+
},
|
|
121
|
+
})) as any;
|
|
122
|
+
|
|
123
|
+
await adapter.create({
|
|
124
|
+
model: "contentItem",
|
|
125
|
+
data: {
|
|
126
|
+
contentTypeId: ct.id,
|
|
127
|
+
slug: "first",
|
|
128
|
+
data: JSON.stringify({ title: "First" }),
|
|
129
|
+
createdAt: new Date(),
|
|
130
|
+
updatedAt: new Date(),
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
await adapter.create({
|
|
134
|
+
model: "contentItem",
|
|
135
|
+
data: {
|
|
136
|
+
contentTypeId: ct.id,
|
|
137
|
+
slug: "second",
|
|
138
|
+
data: JSON.stringify({ title: "Second" }),
|
|
139
|
+
createdAt: new Date(),
|
|
140
|
+
updatedAt: new Date(),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = await getAllContentItems(adapter, "post", {
|
|
145
|
+
slug: "first",
|
|
146
|
+
});
|
|
147
|
+
expect(result.items).toHaveLength(1);
|
|
148
|
+
expect(result.items[0]!.slug).toBe("first");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("getContentItemBySlug", () => {
|
|
153
|
+
it("returns null when content type does not exist", async () => {
|
|
154
|
+
const item = await getContentItemBySlug(adapter, "nonexistent", "item");
|
|
155
|
+
expect(item).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns null when item does not exist", async () => {
|
|
159
|
+
await adapter.create({
|
|
160
|
+
model: "contentType",
|
|
161
|
+
data: {
|
|
162
|
+
name: "Post",
|
|
163
|
+
slug: "post",
|
|
164
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
165
|
+
autoFormVersion: 2,
|
|
166
|
+
createdAt: new Date(),
|
|
167
|
+
updatedAt: new Date(),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const item = await getContentItemBySlug(adapter, "post", "nonexistent");
|
|
172
|
+
expect(item).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("returns the serialized item when it exists", async () => {
|
|
176
|
+
const ct = (await adapter.create({
|
|
177
|
+
model: "contentType",
|
|
178
|
+
data: {
|
|
179
|
+
name: "Post",
|
|
180
|
+
slug: "post",
|
|
181
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
182
|
+
autoFormVersion: 2,
|
|
183
|
+
createdAt: new Date(),
|
|
184
|
+
updatedAt: new Date(),
|
|
185
|
+
},
|
|
186
|
+
})) as any;
|
|
187
|
+
|
|
188
|
+
await adapter.create({
|
|
189
|
+
model: "contentItem",
|
|
190
|
+
data: {
|
|
191
|
+
contentTypeId: ct.id,
|
|
192
|
+
slug: "hello",
|
|
193
|
+
data: JSON.stringify({ title: "Hello" }),
|
|
194
|
+
createdAt: new Date(),
|
|
195
|
+
updatedAt: new Date(),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const item = await getContentItemBySlug(adapter, "post", "hello");
|
|
200
|
+
expect(item).not.toBeNull();
|
|
201
|
+
expect(item!.slug).toBe("hello");
|
|
202
|
+
expect(item!.parsedData).toEqual({ title: "Hello" });
|
|
203
|
+
expect(typeof item!.createdAt).toBe("string");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import type {
|
|
3
|
+
ContentType,
|
|
4
|
+
ContentItem,
|
|
5
|
+
ContentItemWithType,
|
|
6
|
+
SerializedContentType,
|
|
7
|
+
SerializedContentItem,
|
|
8
|
+
SerializedContentItemWithType,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Serialize a ContentType for SSR/SSG use (convert dates to strings).
|
|
13
|
+
* Applies lazy migration for legacy schemas (version 1 → 2).
|
|
14
|
+
*/
|
|
15
|
+
export function serializeContentType(ct: ContentType): SerializedContentType {
|
|
16
|
+
const needsMigration = !ct.autoFormVersion || ct.autoFormVersion < 2;
|
|
17
|
+
const migratedJsonSchema = needsMigration
|
|
18
|
+
? migrateToUnifiedSchema(ct.jsonSchema, ct.fieldConfig)
|
|
19
|
+
: ct.jsonSchema;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
id: ct.id,
|
|
23
|
+
name: ct.name,
|
|
24
|
+
slug: ct.slug,
|
|
25
|
+
description: ct.description,
|
|
26
|
+
jsonSchema: migratedJsonSchema,
|
|
27
|
+
createdAt: ct.createdAt.toISOString(),
|
|
28
|
+
updatedAt: ct.updatedAt.toISOString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function migrateToUnifiedSchema(
|
|
33
|
+
jsonSchemaStr: string,
|
|
34
|
+
fieldConfigStr: string | null | undefined,
|
|
35
|
+
): string {
|
|
36
|
+
if (!fieldConfigStr) return jsonSchemaStr;
|
|
37
|
+
try {
|
|
38
|
+
const jsonSchema = JSON.parse(jsonSchemaStr);
|
|
39
|
+
const fieldConfig = JSON.parse(fieldConfigStr);
|
|
40
|
+
if (!jsonSchema.properties || typeof fieldConfig !== "object") {
|
|
41
|
+
return jsonSchemaStr;
|
|
42
|
+
}
|
|
43
|
+
for (const [key, config] of Object.entries(fieldConfig)) {
|
|
44
|
+
if (
|
|
45
|
+
jsonSchema.properties[key] &&
|
|
46
|
+
typeof config === "object" &&
|
|
47
|
+
config !== null &&
|
|
48
|
+
"fieldType" in config
|
|
49
|
+
) {
|
|
50
|
+
jsonSchema.properties[key].fieldType = (
|
|
51
|
+
config as { fieldType: string }
|
|
52
|
+
).fieldType;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return JSON.stringify(jsonSchema);
|
|
56
|
+
} catch {
|
|
57
|
+
return jsonSchemaStr;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Serialize a ContentItem for SSR/SSG use (convert dates to strings).
|
|
63
|
+
*/
|
|
64
|
+
export function serializeContentItem(item: ContentItem): SerializedContentItem {
|
|
65
|
+
return {
|
|
66
|
+
...item,
|
|
67
|
+
createdAt: item.createdAt.toISOString(),
|
|
68
|
+
updatedAt: item.updatedAt.toISOString(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Serialize a ContentItem with parsed data and joined ContentType.
|
|
74
|
+
* Throws a SyntaxError if `item.data` is not valid JSON, so corrupted rows
|
|
75
|
+
* produce a visible, debuggable error rather than silently returning null.
|
|
76
|
+
*/
|
|
77
|
+
export function serializeContentItemWithType(
|
|
78
|
+
item: ContentItemWithType,
|
|
79
|
+
): SerializedContentItemWithType {
|
|
80
|
+
const parsedData = JSON.parse(item.data) as Record<string, unknown>;
|
|
81
|
+
return {
|
|
82
|
+
...serializeContentItem(item),
|
|
83
|
+
parsedData,
|
|
84
|
+
contentType: item.contentType
|
|
85
|
+
? serializeContentType(item.contentType)
|
|
86
|
+
: undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Retrieve all content types.
|
|
92
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
93
|
+
*
|
|
94
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
95
|
+
* responsible for any access-control checks before invoking this function.
|
|
96
|
+
*
|
|
97
|
+
* @param adapter - The database adapter
|
|
98
|
+
*/
|
|
99
|
+
export async function getAllContentTypes(
|
|
100
|
+
adapter: Adapter,
|
|
101
|
+
): Promise<SerializedContentType[]> {
|
|
102
|
+
const contentTypes = await adapter.findMany<ContentType>({
|
|
103
|
+
model: "contentType",
|
|
104
|
+
sortBy: { field: "name", direction: "asc" },
|
|
105
|
+
});
|
|
106
|
+
return contentTypes.map(serializeContentType);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Retrieve all content items for a given content type, with optional pagination.
|
|
111
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
112
|
+
*
|
|
113
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeListItems`) are NOT
|
|
114
|
+
* called. The caller is responsible for any access-control checks before
|
|
115
|
+
* invoking this function.
|
|
116
|
+
*
|
|
117
|
+
* @param adapter - The database adapter
|
|
118
|
+
* @param contentTypeSlug - The slug of the content type to query
|
|
119
|
+
* @param params - Optional filter/pagination parameters
|
|
120
|
+
*/
|
|
121
|
+
export async function getAllContentItems(
|
|
122
|
+
adapter: Adapter,
|
|
123
|
+
contentTypeSlug: string,
|
|
124
|
+
params?: { slug?: string; limit?: number; offset?: number },
|
|
125
|
+
): Promise<{
|
|
126
|
+
items: SerializedContentItemWithType[];
|
|
127
|
+
total: number;
|
|
128
|
+
limit?: number;
|
|
129
|
+
offset?: number;
|
|
130
|
+
}> {
|
|
131
|
+
const contentType = await adapter.findOne<ContentType>({
|
|
132
|
+
model: "contentType",
|
|
133
|
+
where: [
|
|
134
|
+
{
|
|
135
|
+
field: "slug",
|
|
136
|
+
value: contentTypeSlug,
|
|
137
|
+
operator: "eq" as const,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!contentType) {
|
|
143
|
+
return {
|
|
144
|
+
items: [],
|
|
145
|
+
total: 0,
|
|
146
|
+
limit: params?.limit,
|
|
147
|
+
offset: params?.offset,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const whereConditions: Array<{
|
|
152
|
+
field: string;
|
|
153
|
+
value: string;
|
|
154
|
+
operator: "eq";
|
|
155
|
+
}> = [
|
|
156
|
+
{
|
|
157
|
+
field: "contentTypeId",
|
|
158
|
+
value: contentType.id,
|
|
159
|
+
operator: "eq" as const,
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
if (params?.slug) {
|
|
164
|
+
whereConditions.push({
|
|
165
|
+
field: "slug",
|
|
166
|
+
value: params.slug,
|
|
167
|
+
operator: "eq" as const,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// TODO: remove cast once @btst/db types expose adapter.count()
|
|
172
|
+
const total: number = await adapter.count({
|
|
173
|
+
model: "contentItem",
|
|
174
|
+
where: whereConditions,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const items = await adapter.findMany<ContentItemWithType>({
|
|
178
|
+
model: "contentItem",
|
|
179
|
+
where: whereConditions,
|
|
180
|
+
limit: params?.limit,
|
|
181
|
+
offset: params?.offset,
|
|
182
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
183
|
+
join: { contentType: true },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
items: items.map(serializeContentItemWithType),
|
|
188
|
+
total,
|
|
189
|
+
limit: params?.limit,
|
|
190
|
+
offset: params?.offset,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Retrieve a single content item by its ID.
|
|
196
|
+
* Returns null if the item is not found.
|
|
197
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
198
|
+
*
|
|
199
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
200
|
+
* responsible for any access-control checks before invoking this function.
|
|
201
|
+
*
|
|
202
|
+
* @param adapter - The database adapter
|
|
203
|
+
* @param id - The content item ID (UUID)
|
|
204
|
+
*/
|
|
205
|
+
export async function getContentItemById(
|
|
206
|
+
adapter: Adapter,
|
|
207
|
+
id: string,
|
|
208
|
+
): Promise<SerializedContentItemWithType | null> {
|
|
209
|
+
const item = await adapter.findOne<ContentItemWithType>({
|
|
210
|
+
model: "contentItem",
|
|
211
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
212
|
+
join: { contentType: true },
|
|
213
|
+
});
|
|
214
|
+
if (!item) return null;
|
|
215
|
+
return serializeContentItemWithType(item);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Retrieve a single content item by its slug within a content type.
|
|
220
|
+
* Returns null if the content type or item is not found.
|
|
221
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
222
|
+
*
|
|
223
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
224
|
+
* responsible for any access-control checks before invoking this function.
|
|
225
|
+
*
|
|
226
|
+
* @param adapter - The database adapter
|
|
227
|
+
* @param contentTypeSlug - The slug of the content type
|
|
228
|
+
* @param slug - The slug of the content item
|
|
229
|
+
*/
|
|
230
|
+
export async function getContentItemBySlug(
|
|
231
|
+
adapter: Adapter,
|
|
232
|
+
contentTypeSlug: string,
|
|
233
|
+
slug: string,
|
|
234
|
+
): Promise<SerializedContentItemWithType | null> {
|
|
235
|
+
const contentType = await adapter.findOne<ContentType>({
|
|
236
|
+
model: "contentType",
|
|
237
|
+
where: [
|
|
238
|
+
{
|
|
239
|
+
field: "slug",
|
|
240
|
+
value: contentTypeSlug,
|
|
241
|
+
operator: "eq" as const,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!contentType) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const item = await adapter.findOne<ContentItemWithType>({
|
|
251
|
+
model: "contentItem",
|
|
252
|
+
where: [
|
|
253
|
+
{
|
|
254
|
+
field: "contentTypeId",
|
|
255
|
+
value: contentType.id,
|
|
256
|
+
operator: "eq" as const,
|
|
257
|
+
},
|
|
258
|
+
{ field: "slug", value: slug, operator: "eq" as const },
|
|
259
|
+
],
|
|
260
|
+
join: { contentType: true },
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!item) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return serializeContentItemWithType(item);
|
|
268
|
+
}
|
|
@@ -1 +1,15 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
cmsBackendPlugin,
|
|
3
|
+
type CMSApiRouter,
|
|
4
|
+
type CMSRouteKey,
|
|
5
|
+
} from "./plugin";
|
|
6
|
+
export {
|
|
7
|
+
getAllContentTypes,
|
|
8
|
+
getAllContentItems,
|
|
9
|
+
getContentItemBySlug,
|
|
10
|
+
getContentItemById,
|
|
11
|
+
serializeContentType,
|
|
12
|
+
serializeContentItem,
|
|
13
|
+
serializeContentItemWithType,
|
|
14
|
+
} from "./getters";
|
|
15
|
+
export { CMS_QUERY_KEYS } from "./query-key-defs";
|