@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
@@ -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<Post[]>)
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
- // Type narrowed to Data<Post[]> after error check
108
- return ((response as { data?: unknown }).data ??
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<Post[]>)
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<Post[]> after error check
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<Post[]>)
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
- // Type narrowed to Data<Post[]> after error check
190
- let posts = ((response as { data?: unknown }).data ??
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<Post[]>)
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
- // Type narrowed to Data<Post[]> after error check
237
- return ((response as { data?: unknown }).data ??
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 { cmsBackendPlugin, type CMSApiRouter } from "./plugin";
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";