@btst/stack 2.2.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 (159) hide show
  1. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +52 -1
  2. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +52 -1
  3. package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
  4. package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
  5. package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
  6. package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
  7. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +15 -0
  8. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +16 -1
  9. package/dist/packages/stack/src/plugins/cms/api/getters.cjs +10 -0
  10. package/dist/packages/stack/src/plugins/cms/api/getters.mjs +10 -1
  11. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +70 -1
  12. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +71 -2
  13. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
  14. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
  15. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +15 -0
  16. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
  17. package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +9 -0
  18. package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +9 -1
  19. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +62 -1
  20. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +63 -2
  21. package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
  22. package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
  23. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +15 -0
  24. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -1
  25. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +29 -1
  26. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +29 -1
  27. package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
  28. package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
  29. package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
  30. package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
  31. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +10 -0
  32. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +11 -1
  33. package/dist/packages/stack/src/plugins/utils.cjs +6 -0
  34. package/dist/packages/stack/src/plugins/utils.mjs +6 -1
  35. package/dist/plugins/blog/api/index.cjs +5 -0
  36. package/dist/plugins/blog/api/index.d.cts +19 -4
  37. package/dist/plugins/blog/api/index.d.mts +19 -4
  38. package/dist/plugins/blog/api/index.d.ts +19 -4
  39. package/dist/plugins/blog/api/index.mjs +2 -0
  40. package/dist/plugins/blog/client/hooks/index.d.cts +4 -4
  41. package/dist/plugins/blog/client/hooks/index.d.mts +4 -4
  42. package/dist/plugins/blog/client/hooks/index.d.ts +4 -4
  43. package/dist/plugins/blog/client/index.d.cts +1 -1
  44. package/dist/plugins/blog/client/index.d.mts +1 -1
  45. package/dist/plugins/blog/client/index.d.ts +1 -1
  46. package/dist/plugins/blog/query-keys.cjs +6 -5
  47. package/dist/plugins/blog/query-keys.d.cts +8 -387
  48. package/dist/plugins/blog/query-keys.d.mts +8 -387
  49. package/dist/plugins/blog/query-keys.d.ts +8 -387
  50. package/dist/plugins/blog/query-keys.mjs +6 -5
  51. package/dist/plugins/client/index.cjs +1 -0
  52. package/dist/plugins/client/index.d.cts +8 -1
  53. package/dist/plugins/client/index.d.mts +8 -1
  54. package/dist/plugins/client/index.d.ts +8 -1
  55. package/dist/plugins/client/index.mjs +1 -1
  56. package/dist/plugins/cms/api/index.cjs +6 -0
  57. package/dist/plugins/cms/api/index.d.cts +7 -219
  58. package/dist/plugins/cms/api/index.d.mts +7 -219
  59. package/dist/plugins/cms/api/index.d.ts +7 -219
  60. package/dist/plugins/cms/api/index.mjs +2 -1
  61. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  62. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  63. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  64. package/dist/plugins/cms/query-keys.cjs +2 -1
  65. package/dist/plugins/cms/query-keys.d.cts +5 -9
  66. package/dist/plugins/cms/query-keys.d.mts +5 -9
  67. package/dist/plugins/cms/query-keys.d.ts +5 -9
  68. package/dist/plugins/cms/query-keys.mjs +2 -1
  69. package/dist/plugins/form-builder/api/index.cjs +6 -0
  70. package/dist/plugins/form-builder/api/index.d.cts +7 -211
  71. package/dist/plugins/form-builder/api/index.d.mts +7 -211
  72. package/dist/plugins/form-builder/api/index.d.ts +7 -211
  73. package/dist/plugins/form-builder/api/index.mjs +2 -1
  74. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  75. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  76. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  77. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  78. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  79. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  80. package/dist/plugins/form-builder/query-keys.cjs +3 -2
  81. package/dist/plugins/form-builder/query-keys.d.cts +6 -6
  82. package/dist/plugins/form-builder/query-keys.d.mts +6 -6
  83. package/dist/plugins/form-builder/query-keys.d.ts +6 -6
  84. package/dist/plugins/form-builder/query-keys.mjs +3 -2
  85. package/dist/plugins/kanban/api/index.cjs +6 -0
  86. package/dist/plugins/kanban/api/index.d.cts +17 -392
  87. package/dist/plugins/kanban/api/index.d.mts +17 -392
  88. package/dist/plugins/kanban/api/index.d.ts +17 -392
  89. package/dist/plugins/kanban/api/index.mjs +2 -0
  90. package/dist/plugins/kanban/client/components/index.d.cts +1 -1
  91. package/dist/plugins/kanban/client/components/index.d.mts +1 -1
  92. package/dist/plugins/kanban/client/components/index.d.ts +1 -1
  93. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  94. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  95. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  96. package/dist/plugins/kanban/client/index.d.cts +1 -1
  97. package/dist/plugins/kanban/client/index.d.mts +1 -1
  98. package/dist/plugins/kanban/client/index.d.ts +1 -1
  99. package/dist/plugins/kanban/query-keys.cjs +2 -9
  100. package/dist/plugins/kanban/query-keys.d.cts +4 -16
  101. package/dist/plugins/kanban/query-keys.d.mts +4 -16
  102. package/dist/plugins/kanban/query-keys.d.ts +4 -16
  103. package/dist/plugins/kanban/query-keys.mjs +2 -9
  104. package/dist/plugins/ui-builder/index.d.cts +1 -1
  105. package/dist/plugins/ui-builder/index.d.mts +1 -1
  106. package/dist/plugins/ui-builder/index.d.ts +1 -1
  107. package/dist/shared/stack.B1EeBt1b.d.ts +297 -0
  108. package/dist/shared/stack.BIXEI6v_.d.mts +419 -0
  109. package/dist/shared/stack.BKfolAyK.d.ts +419 -0
  110. package/dist/shared/stack.BpolpQpf.d.cts +445 -0
  111. package/dist/shared/stack.C5dtIncc.d.mts +293 -0
  112. package/dist/shared/stack.CIP6QS9l.d.ts +293 -0
  113. package/dist/shared/stack.CP68pFEH.d.mts +297 -0
  114. package/dist/shared/{stack.CXjzTMsb.d.mts → stack.CVDTkMoO.d.cts} +7 -1
  115. package/dist/shared/{stack.CXjzTMsb.d.ts → stack.CVDTkMoO.d.mts} +7 -1
  116. package/dist/shared/{stack.CXjzTMsb.d.cts → stack.CVDTkMoO.d.ts} +7 -1
  117. package/dist/shared/{stack.QD1y_7NY.d.mts → stack.DJaKVY7v.d.cts} +1 -1
  118. package/dist/shared/{stack.QD1y_7NY.d.ts → stack.DJaKVY7v.d.mts} +1 -1
  119. package/dist/shared/{stack.QD1y_7NY.d.cts → stack.DJaKVY7v.d.ts} +1 -1
  120. package/dist/shared/{stack.CIrIsc-A.d.mts → stack.DdI5W6MB.d.cts} +7 -1
  121. package/dist/shared/{stack.CIrIsc-A.d.ts → stack.DdI5W6MB.d.mts} +7 -1
  122. package/dist/shared/{stack.CIrIsc-A.d.cts → stack.DdI5W6MB.d.ts} +7 -1
  123. package/dist/shared/stack.Dw0Ly2TM.d.cts +293 -0
  124. package/dist/shared/stack.IdtKDRka.d.cts +297 -0
  125. package/dist/shared/stack.TIBF2AOx.d.ts +445 -0
  126. package/dist/shared/stack.rTy7-wQU.d.mts +445 -0
  127. package/dist/shared/stack.snB1EDP7.d.cts +419 -0
  128. package/package.json +3 -3
  129. package/src/plugins/blog/api/index.ts +2 -0
  130. package/src/plugins/blog/api/plugin.ts +85 -0
  131. package/src/plugins/blog/api/query-key-defs.ts +46 -0
  132. package/src/plugins/blog/api/serializers.ts +27 -0
  133. package/src/plugins/blog/client/plugin.tsx +19 -0
  134. package/src/plugins/blog/query-keys.ts +5 -7
  135. package/src/plugins/client/index.ts +1 -1
  136. package/src/plugins/cms/api/getters.ts +24 -0
  137. package/src/plugins/cms/api/index.ts +10 -1
  138. package/src/plugins/cms/api/plugin.ts +105 -0
  139. package/src/plugins/cms/api/query-key-defs.ts +53 -0
  140. package/src/plugins/cms/api/serializers.ts +12 -0
  141. package/src/plugins/cms/client/plugin.tsx +19 -0
  142. package/src/plugins/cms/query-keys.ts +2 -1
  143. package/src/plugins/form-builder/api/getters.ts +23 -0
  144. package/src/plugins/form-builder/api/index.ts +15 -2
  145. package/src/plugins/form-builder/api/plugin.ts +91 -0
  146. package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
  147. package/src/plugins/form-builder/api/serializers.ts +12 -0
  148. package/src/plugins/form-builder/client/plugin.tsx +19 -0
  149. package/src/plugins/form-builder/query-keys.ts +6 -2
  150. package/src/plugins/kanban/api/index.ts +3 -0
  151. package/src/plugins/kanban/api/plugin.ts +49 -0
  152. package/src/plugins/kanban/api/query-key-defs.ts +54 -0
  153. package/src/plugins/kanban/api/serializers.ts +49 -0
  154. package/src/plugins/kanban/client/plugin.tsx +13 -0
  155. package/src/plugins/kanban/query-keys.ts +2 -9
  156. package/src/plugins/utils.ts +19 -0
  157. package/dist/shared/{stack.BkYlUT_8.d.cts → stack.CBON0dWL.d.cts} +6 -6
  158. package/dist/shared/{stack.BkYlUT_8.d.mts → stack.CBON0dWL.d.mts} +6 -6
  159. package/dist/shared/{stack.BkYlUT_8.d.ts → stack.CBON0dWL.d.ts} +6 -6
@@ -0,0 +1,419 @@
1
+ import * as _tanstack_react_query from '@tanstack/react-query';
2
+ import { QueryClient } from '@tanstack/react-query';
3
+ import { createApiClient } from '@btst/stack/plugins/client';
4
+ import { P as Post, T as Tag, c as createPostSchema, u as updatePostSchema, S as SerializedPost, a as SerializedTag } from './stack.CBON0dWL.cjs';
5
+ import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
6
+ import * as better_call from 'better-call';
7
+ import { Adapter } from '@btst/db';
8
+ import { z } from 'zod';
9
+
10
+ /**
11
+ * Parameters for filtering/paginating posts.
12
+ * Mirrors the shape of the list API query schema.
13
+ */
14
+ interface PostListParams {
15
+ slug?: string;
16
+ tagSlug?: string;
17
+ offset?: number;
18
+ limit?: number;
19
+ query?: string;
20
+ published?: boolean;
21
+ }
22
+ /**
23
+ * Paginated result returned by {@link getAllPosts}.
24
+ */
25
+ interface PostListResult {
26
+ items: Array<Post & {
27
+ tags: Tag[];
28
+ }>;
29
+ total: number;
30
+ limit?: number;
31
+ offset?: number;
32
+ }
33
+ /**
34
+ * Retrieve all posts matching optional filter criteria.
35
+ * Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
36
+ *
37
+ * @remarks **Security:** Authorization hooks (e.g. `onBeforeListPosts`) are NOT
38
+ * called. The caller is responsible for any access-control checks before
39
+ * invoking this function.
40
+ *
41
+ * @param adapter - The database adapter
42
+ * @param params - Optional filter/pagination parameters (same shape as the list API query)
43
+ */
44
+ declare function getAllPosts(adapter: Adapter, params?: PostListParams): Promise<PostListResult>;
45
+ /**
46
+ * Retrieve a single post by its slug, including associated tags.
47
+ * Returns null if no post is found.
48
+ * Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
49
+ *
50
+ * @remarks **Security:** Authorization hooks are NOT called. The caller is
51
+ * responsible for any access-control checks before invoking this function.
52
+ *
53
+ * @param adapter - The database adapter
54
+ * @param slug - The post slug
55
+ */
56
+ declare function getPostBySlug(adapter: Adapter, slug: string): Promise<(Post & {
57
+ tags: Tag[];
58
+ }) | null>;
59
+ /**
60
+ * Retrieve all tags, sorted alphabetically by name.
61
+ * Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
62
+ *
63
+ * @remarks **Security:** Authorization hooks are NOT called. The caller is
64
+ * responsible for any access-control checks before invoking this function.
65
+ *
66
+ * @param adapter - The database adapter
67
+ */
68
+ declare function getAllTags(adapter: Adapter): Promise<Tag[]>;
69
+
70
+ /**
71
+ * Route keys for the blog plugin — matches the keys returned by
72
+ * `stackClient.router.getRoute(path).routeKey`.
73
+ */
74
+ type BlogRouteKey = "posts" | "drafts" | "post" | "tag" | "newPost" | "editPost";
75
+ /**
76
+ * Overloaded signature for `prefetchForRoute`.
77
+ * TypeScript enforces the correct params for each routeKey at call sites.
78
+ */
79
+ interface BlogPrefetchForRoute {
80
+ (key: "posts" | "drafts" | "newPost", qc: QueryClient): Promise<void>;
81
+ (key: "post" | "editPost", qc: QueryClient, params: {
82
+ slug: string;
83
+ }): Promise<void>;
84
+ (key: "tag", qc: QueryClient, params: {
85
+ tagSlug: string;
86
+ }): Promise<void>;
87
+ }
88
+ declare const PostListQuerySchema: z.ZodObject<{
89
+ slug: z.ZodOptional<z.ZodString>;
90
+ tagSlug: z.ZodOptional<z.ZodString>;
91
+ offset: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
92
+ limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
93
+ query: z.ZodOptional<z.ZodString>;
94
+ published: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<boolean | undefined, string | undefined>>;
95
+ }, z.core.$strip>;
96
+ declare const NextPreviousPostsQuerySchema: z.ZodObject<{
97
+ date: z.ZodCoercedDate<unknown>;
98
+ }, z.core.$strip>;
99
+ /**
100
+ * Context passed to blog API hooks
101
+ */
102
+ interface BlogApiContext<TBody = any, TParams = any, TQuery = any> {
103
+ body?: TBody;
104
+ params?: TParams;
105
+ query?: TQuery;
106
+ request?: Request;
107
+ headers?: Headers;
108
+ [key: string]: any;
109
+ }
110
+ /**
111
+ * Configuration hooks for blog backend plugin
112
+ * All hooks are optional and allow consumers to customize behavior
113
+ */
114
+ interface BlogBackendHooks {
115
+ /**
116
+ * Called before listing posts. Return false to deny access.
117
+ * @param filter - Query parameters for filtering posts
118
+ * @param context - Request context with headers, etc.
119
+ */
120
+ onBeforeListPosts?: (filter: z.infer<typeof PostListQuerySchema>, context: BlogApiContext) => Promise<boolean> | boolean;
121
+ /**
122
+ * Called before creating a post. Return false to deny access.
123
+ * @param data - Post data being created
124
+ * @param context - Request context with headers, etc.
125
+ */
126
+ onBeforeCreatePost?: (data: z.infer<typeof createPostSchema>, context: BlogApiContext) => Promise<boolean> | boolean;
127
+ /**
128
+ * Called before updating a post. Return false to deny access.
129
+ * @param postId - ID of the post being updated
130
+ * @param data - Updated post data
131
+ * @param context - Request context with headers, etc.
132
+ */
133
+ onBeforeUpdatePost?: (postId: string, data: z.infer<typeof updatePostSchema>, context: BlogApiContext) => Promise<boolean> | boolean;
134
+ /**
135
+ * Called before deleting a post. Return false to deny access.
136
+ * @param postId - ID of the post being deleted
137
+ * @param context - Request context with headers, etc.
138
+ */
139
+ onBeforeDeletePost?: (postId: string, context: BlogApiContext) => Promise<boolean> | boolean;
140
+ /**
141
+ * Called after posts are read successfully
142
+ * @param posts - The list of posts returned by the query
143
+ * @param filter - Query parameters used for filtering
144
+ * @param context - Request context
145
+ */
146
+ onPostsRead?: (posts: Array<Post & {
147
+ tags: Tag[];
148
+ }>, filter: z.infer<typeof PostListQuerySchema>, context: BlogApiContext) => Promise<void> | void;
149
+ /**
150
+ * Called after a post is created successfully
151
+ * @param post - The created post
152
+ * @param context - Request context
153
+ */
154
+ onPostCreated?: (post: Post, context: BlogApiContext) => Promise<void> | void;
155
+ /**
156
+ * Called after a post is updated successfully
157
+ * @param post - The updated post
158
+ * @param context - Request context
159
+ */
160
+ onPostUpdated?: (post: Post, context: BlogApiContext) => Promise<void> | void;
161
+ /**
162
+ * Called after a post is deleted successfully
163
+ * @param postId - ID of the deleted post
164
+ * @param context - Request context
165
+ */
166
+ onPostDeleted?: (postId: string, context: BlogApiContext) => Promise<void> | void;
167
+ /**
168
+ * Called when listing posts fails
169
+ * @param error - The error that occurred
170
+ * @param context - Request context
171
+ */
172
+ onListPostsError?: (error: Error, context: BlogApiContext) => Promise<void> | void;
173
+ /**
174
+ * Called when creating a post fails
175
+ * @param error - The error that occurred
176
+ * @param context - Request context
177
+ */
178
+ onCreatePostError?: (error: Error, context: BlogApiContext) => Promise<void> | void;
179
+ /**
180
+ * Called when updating a post fails
181
+ * @param error - The error that occurred
182
+ * @param context - Request context
183
+ */
184
+ onUpdatePostError?: (error: Error, context: BlogApiContext) => Promise<void> | void;
185
+ /**
186
+ * Called when deleting a post fails
187
+ * @param error - The error that occurred
188
+ * @param context - Request context
189
+ */
190
+ onDeletePostError?: (error: Error, context: BlogApiContext) => Promise<void> | void;
191
+ }
192
+ /**
193
+ * Blog backend plugin
194
+ * Provides API endpoints for managing blog posts
195
+ * Uses better-db adapter for database operations
196
+ *
197
+ * @param hooks - Optional configuration hooks for customizing plugin behavior
198
+ */
199
+ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugins_api.BackendPlugin<{
200
+ readonly listPosts: better_call.StrictEndpoint<"/posts", {
201
+ method: "GET";
202
+ query: z.ZodObject<{
203
+ slug: z.ZodOptional<z.ZodString>;
204
+ tagSlug: z.ZodOptional<z.ZodString>;
205
+ offset: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
206
+ limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
207
+ query: z.ZodOptional<z.ZodString>;
208
+ published: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<boolean | undefined, string | undefined>>;
209
+ }, z.core.$strip>;
210
+ }, PostListResult>;
211
+ readonly createPost: better_call.StrictEndpoint<"/posts", {
212
+ method: "POST";
213
+ body: z.ZodObject<{
214
+ tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
215
+ name: z.ZodString;
216
+ }, z.core.$strip>, z.ZodObject<{
217
+ id: z.ZodString;
218
+ name: z.ZodString;
219
+ slug: z.ZodString;
220
+ }, z.core.$strip>]>>>>;
221
+ slug: z.ZodOptional<z.ZodString>;
222
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
223
+ createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
224
+ updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
225
+ title: z.ZodString;
226
+ content: z.ZodString;
227
+ excerpt: z.ZodString;
228
+ image: z.ZodOptional<z.ZodString>;
229
+ published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
230
+ }, z.core.$strip>;
231
+ }, Post>;
232
+ readonly updatePost: better_call.StrictEndpoint<"/posts/:id", {
233
+ method: "PUT";
234
+ body: z.ZodObject<{
235
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
236
+ createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
237
+ updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
238
+ title: z.ZodString;
239
+ content: z.ZodString;
240
+ excerpt: z.ZodString;
241
+ image: z.ZodOptional<z.ZodString>;
242
+ published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
243
+ slug: z.ZodString;
244
+ tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
245
+ name: z.ZodString;
246
+ }, z.core.$strip>, z.ZodObject<{
247
+ id: z.ZodString;
248
+ name: z.ZodString;
249
+ slug: z.ZodString;
250
+ }, z.core.$strip>]>>>>;
251
+ id: z.ZodString;
252
+ }, z.core.$strip>;
253
+ }, Post>;
254
+ readonly deletePost: better_call.StrictEndpoint<"/posts/:id", {
255
+ method: "DELETE";
256
+ }, {
257
+ success: boolean;
258
+ }>;
259
+ readonly getNextPreviousPosts: better_call.StrictEndpoint<"/posts/next-previous", {
260
+ method: "GET";
261
+ query: z.ZodObject<{
262
+ date: z.ZodCoercedDate<unknown>;
263
+ }, z.core.$strip>;
264
+ }, {
265
+ previous: {
266
+ tags: Tag[];
267
+ id: string;
268
+ authorId?: string;
269
+ defaultLocale?: string;
270
+ slug: string;
271
+ title: string;
272
+ content: string;
273
+ excerpt: string;
274
+ image?: string;
275
+ published: boolean;
276
+ status?: "DRAFT" | "PUBLISHED";
277
+ publishedAt?: Date;
278
+ createdAt: Date;
279
+ updatedAt: Date;
280
+ } | null;
281
+ next: {
282
+ tags: Tag[];
283
+ id: string;
284
+ authorId?: string;
285
+ defaultLocale?: string;
286
+ slug: string;
287
+ title: string;
288
+ content: string;
289
+ excerpt: string;
290
+ image?: string;
291
+ published: boolean;
292
+ status?: "DRAFT" | "PUBLISHED";
293
+ publishedAt?: Date;
294
+ createdAt: Date;
295
+ updatedAt: Date;
296
+ } | null;
297
+ }>;
298
+ readonly listTags: better_call.StrictEndpoint<"/tags", {
299
+ method: "GET";
300
+ }, Tag[]>;
301
+ }, {
302
+ getAllPosts: (params?: Parameters<typeof getAllPosts>[1]) => Promise<PostListResult>;
303
+ getPostBySlug: (slug: string) => Promise<(Post & {
304
+ tags: Tag[];
305
+ }) | null>;
306
+ getAllTags: () => Promise<Tag[]>;
307
+ prefetchForRoute: BlogPrefetchForRoute;
308
+ }>;
309
+ type BlogApiRouter = ReturnType<ReturnType<typeof blogBackendPlugin>["routes"]>;
310
+
311
+ /**
312
+ * Internal query key constants for the blog plugin.
313
+ * Shared between query-keys.ts (HTTP path) and prefetchForRoute (DB path)
314
+ * to prevent key drift between SSR loaders and SSG prefetching.
315
+ */
316
+ interface PostsListDiscriminator {
317
+ query: string | undefined;
318
+ limit: number;
319
+ published: boolean;
320
+ tagSlug: string | undefined;
321
+ }
322
+ /** Full query key builders — use these with queryClient.setQueryData() */
323
+ declare const BLOG_QUERY_KEYS: {
324
+ postsList: (params: {
325
+ published: boolean;
326
+ limit?: number;
327
+ tagSlug?: string;
328
+ }) => readonly ["posts", "list", PostsListDiscriminator];
329
+ postDetail: (slug: string) => readonly ["posts", "detail", string];
330
+ tagsList: () => readonly ["tags", "list", "tags"];
331
+ };
332
+
333
+ interface PostsListParams {
334
+ query?: string;
335
+ limit?: number;
336
+ published?: boolean;
337
+ tagSlug?: string;
338
+ }
339
+ declare function createBlogQueryKeys(client: ReturnType<typeof createApiClient<BlogApiRouter>>, headers?: HeadersInit): {
340
+ posts: {
341
+ _def: readonly ["posts"];
342
+ } & {
343
+ list: ((params?: PostsListParams | undefined) => Omit<Omit<{
344
+ queryKey: readonly ["posts", "list", PostsListDiscriminator];
345
+ queryFn: _tanstack_react_query.QueryFunction<unknown, readonly ["posts", "list", PostsListDiscriminator]>;
346
+ }, "queryFn"> & {
347
+ _def: readonly ["posts", "list"];
348
+ }, "_def">) & {
349
+ _def: readonly ["posts", "list"];
350
+ };
351
+ detail: ((slug: string) => Omit<{
352
+ queryKey: readonly ["posts", "detail", string];
353
+ queryFn: _tanstack_react_query.QueryFunction<SerializedPost | null, readonly ["posts", "detail", string]>;
354
+ } & {
355
+ _def: readonly ["posts", "detail"];
356
+ }, "_def">) & {
357
+ _def: readonly ["posts", "detail"];
358
+ };
359
+ nextPrevious: ((date: string | Date) => Omit<{
360
+ queryKey: readonly ["posts", "nextPrevious", string, string | Date];
361
+ queryFn: _tanstack_react_query.QueryFunction<{
362
+ previous: SerializedPost | null;
363
+ next: SerializedPost | null;
364
+ }, readonly ["posts", "nextPrevious", string, string | Date]>;
365
+ } & {
366
+ _def: readonly ["posts", "nextPrevious"];
367
+ }, "_def">) & {
368
+ _def: readonly ["posts", "nextPrevious"];
369
+ };
370
+ recent: ((params?: {
371
+ limit?: number;
372
+ excludeSlug?: string;
373
+ } | undefined) => Omit<{
374
+ queryKey: readonly ["posts", "recent", string, {
375
+ limit?: number;
376
+ excludeSlug?: string;
377
+ } | undefined];
378
+ queryFn: _tanstack_react_query.QueryFunction<SerializedPost[], readonly ["posts", "recent", string, {
379
+ limit?: number;
380
+ excludeSlug?: string;
381
+ } | undefined]>;
382
+ } & {
383
+ _def: readonly ["posts", "recent"];
384
+ }, "_def">) & {
385
+ _def: readonly ["posts", "recent"];
386
+ };
387
+ };
388
+ drafts: {
389
+ _def: readonly ["drafts"];
390
+ } & {
391
+ list: ((params?: PostsListParams | undefined) => Omit<Omit<{
392
+ queryKey: readonly ["drafts", "list", {
393
+ limit?: number | undefined;
394
+ }];
395
+ queryFn: _tanstack_react_query.QueryFunction<unknown, readonly ["drafts", "list", {
396
+ limit?: number | undefined;
397
+ }]>;
398
+ }, "queryFn"> & {
399
+ _def: readonly ["drafts", "list"];
400
+ }, "_def">) & {
401
+ _def: readonly ["drafts", "list"];
402
+ };
403
+ };
404
+ tags: {
405
+ _def: readonly ["tags"];
406
+ } & {
407
+ list: (() => Omit<{
408
+ queryKey: readonly ["tags", "list", string];
409
+ queryFn: _tanstack_react_query.QueryFunction<SerializedTag[], readonly ["tags", "list", string]>;
410
+ } & {
411
+ _def: readonly ["tags", "list"];
412
+ }, "_def">) & {
413
+ _def: readonly ["tags", "list"];
414
+ };
415
+ };
416
+ };
417
+
418
+ export { BLOG_QUERY_KEYS as B, NextPreviousPostsQuerySchema as N, getPostBySlug as a, getAllTags as b, createBlogQueryKeys as d, PostListQuerySchema as f, getAllPosts as g, blogBackendPlugin as j };
419
+ export type { PostListParams as P, PostListResult as c, BlogRouteKey as e, BlogApiContext as h, BlogBackendHooks as i, BlogApiRouter as k };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -458,7 +458,7 @@
458
458
  },
459
459
  "peerDependencies": {
460
460
  "@ai-sdk/react": ">=2.0.0",
461
- "@btst/yar": ">=1.1.0",
461
+ "@btst/yar": ">=1.2.0",
462
462
  "@hookform/resolvers": ">=5.0.0",
463
463
  "@radix-ui/react-dialog": ">=1.1.0",
464
464
  "@radix-ui/react-label": ">=2.1.0",
@@ -494,7 +494,7 @@
494
494
  "devDependencies": {
495
495
  "@ai-sdk/react": "^2.0.94",
496
496
  "@btst/adapter-memory": "2.0.3",
497
- "@btst/yar": "1.1.1",
497
+ "@btst/yar": "1.2.0",
498
498
  "@types/react": "^19.0.0",
499
499
  "@types/slug": "^5.0.9",
500
500
  "@workspace/ui": "workspace:*",
@@ -6,4 +6,6 @@ export {
6
6
  type PostListParams,
7
7
  type PostListResult,
8
8
  } from "./getters";
9
+ export { serializePost, serializeTag } from "./serializers";
10
+ export { BLOG_QUERY_KEYS } from "./query-key-defs";
9
11
  export { createBlogQueryKeys } from "../query-keys";
@@ -7,6 +7,90 @@ import type { Post, PostWithPostTag, Tag } from "../types";
7
7
  import { slugify } from "../utils";
8
8
  import { createPostSchema, updatePostSchema } from "../schemas";
9
9
  import { getAllPosts, getPostBySlug, getAllTags } from "./getters";
10
+ import { BLOG_QUERY_KEYS } from "./query-key-defs";
11
+ import { serializePost, serializeTag } from "./serializers";
12
+ import type { QueryClient } from "@tanstack/react-query";
13
+
14
+ /**
15
+ * Route keys for the blog plugin — matches the keys returned by
16
+ * `stackClient.router.getRoute(path).routeKey`.
17
+ */
18
+ export type BlogRouteKey =
19
+ | "posts"
20
+ | "drafts"
21
+ | "post"
22
+ | "tag"
23
+ | "newPost"
24
+ | "editPost";
25
+
26
+ /**
27
+ * Overloaded signature for `prefetchForRoute`.
28
+ * TypeScript enforces the correct params for each routeKey at call sites.
29
+ */
30
+ interface BlogPrefetchForRoute {
31
+ (key: "posts" | "drafts" | "newPost", qc: QueryClient): Promise<void>;
32
+ (
33
+ key: "post" | "editPost",
34
+ qc: QueryClient,
35
+ params: { slug: string },
36
+ ): Promise<void>;
37
+ (key: "tag", qc: QueryClient, params: { tagSlug: string }): Promise<void>;
38
+ }
39
+
40
+ function createBlogPrefetchForRoute(adapter: Adapter): BlogPrefetchForRoute {
41
+ return async function prefetchForRoute(
42
+ key: BlogRouteKey,
43
+ qc: QueryClient,
44
+ params?: Record<string, string>,
45
+ ): Promise<void> {
46
+ switch (key) {
47
+ case "posts":
48
+ case "drafts": {
49
+ const published = key === "posts";
50
+ const [result, tags] = await Promise.all([
51
+ getAllPosts(adapter, { published, limit: 10 }),
52
+ getAllTags(adapter),
53
+ ]);
54
+ qc.setQueryData(BLOG_QUERY_KEYS.postsList({ published, limit: 10 }), {
55
+ pages: [result.items.map(serializePost)],
56
+ pageParams: [0],
57
+ });
58
+ qc.setQueryData(BLOG_QUERY_KEYS.tagsList(), tags.map(serializeTag));
59
+ break;
60
+ }
61
+ case "post":
62
+ case "editPost": {
63
+ const slug = params?.slug ?? "";
64
+ if (slug) {
65
+ const post = await getPostBySlug(adapter, slug);
66
+ qc.setQueryData(
67
+ BLOG_QUERY_KEYS.postDetail(slug),
68
+ post ? serializePost(post) : null,
69
+ );
70
+ }
71
+ break;
72
+ }
73
+ case "tag": {
74
+ const tagSlug = params?.tagSlug ?? "";
75
+ const [result, tags] = await Promise.all([
76
+ getAllPosts(adapter, { published: true, limit: 10, tagSlug }),
77
+ getAllTags(adapter),
78
+ ]);
79
+ qc.setQueryData(
80
+ BLOG_QUERY_KEYS.postsList({ published: true, limit: 10, tagSlug }),
81
+ {
82
+ pages: [result.items.map(serializePost)],
83
+ pageParams: [0],
84
+ },
85
+ );
86
+ qc.setQueryData(BLOG_QUERY_KEYS.tagsList(), tags.map(serializeTag));
87
+ break;
88
+ }
89
+ default:
90
+ break;
91
+ }
92
+ } as BlogPrefetchForRoute;
93
+ }
10
94
 
11
95
  export const PostListQuerySchema = z.object({
12
96
  slug: z.string().optional(),
@@ -174,6 +258,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
174
258
  getAllPosts(adapter, params),
175
259
  getPostBySlug: (slug: string) => getPostBySlug(adapter, slug),
176
260
  getAllTags: () => getAllTags(adapter),
261
+ prefetchForRoute: createBlogPrefetchForRoute(adapter),
177
262
  }),
178
263
 
179
264
  routes: (adapter: Adapter) => {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Internal query key constants for the blog plugin.
3
+ * Shared between query-keys.ts (HTTP path) and prefetchForRoute (DB path)
4
+ * to prevent key drift between SSR loaders and SSG prefetching.
5
+ */
6
+
7
+ export interface PostsListDiscriminator {
8
+ query: string | undefined;
9
+ limit: number;
10
+ published: boolean;
11
+ tagSlug: string | undefined;
12
+ }
13
+
14
+ /**
15
+ * Builds the discriminator object used as the cache key for the posts list.
16
+ * Mirrors the inline object in createPostsQueries so both paths stay in sync.
17
+ */
18
+ export function postsListDiscriminator(params: {
19
+ published: boolean;
20
+ limit?: number;
21
+ tagSlug?: string;
22
+ query?: string;
23
+ }): PostsListDiscriminator {
24
+ return {
25
+ query:
26
+ params.query !== undefined && params.query.trim() === ""
27
+ ? undefined
28
+ : params.query,
29
+ limit: params.limit ?? 10,
30
+ published: params.published,
31
+ tagSlug: params.tagSlug,
32
+ };
33
+ }
34
+
35
+ /** Full query key builders — use these with queryClient.setQueryData() */
36
+ export const BLOG_QUERY_KEYS = {
37
+ postsList: (params: {
38
+ published: boolean;
39
+ limit?: number;
40
+ tagSlug?: string;
41
+ }) => ["posts", "list", postsListDiscriminator(params)] as const,
42
+
43
+ postDetail: (slug: string) => ["posts", "detail", slug] as const,
44
+
45
+ tagsList: () => ["tags", "list", "tags"] as const,
46
+ };
@@ -0,0 +1,27 @@
1
+ import type { Post, Tag, SerializedPost, SerializedTag } from "../types";
2
+
3
+ /**
4
+ * Serialize a Tag for SSR/SSG use (convert dates to strings).
5
+ * Pure function — no DB access, no hooks.
6
+ */
7
+ export function serializeTag(tag: Tag): SerializedTag {
8
+ return {
9
+ ...tag,
10
+ createdAt: tag.createdAt.toISOString(),
11
+ updatedAt: tag.updatedAt.toISOString(),
12
+ };
13
+ }
14
+
15
+ /**
16
+ * Serialize a Post (with tags) for SSR/SSG use (convert dates to strings).
17
+ * Pure function — no DB access, no hooks.
18
+ */
19
+ export function serializePost(post: Post & { tags: Tag[] }): SerializedPost {
20
+ return {
21
+ ...post,
22
+ createdAt: post.createdAt.toISOString(),
23
+ updatedAt: post.updatedAt.toISOString(),
24
+ publishedAt: post.publishedAt?.toISOString(),
25
+ tags: post.tags.map(serializeTag),
26
+ };
27
+ }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  defineClientPlugin,
3
3
  createApiClient,
4
+ isConnectionError,
4
5
  } from "@btst/stack/plugins/client";
5
6
  import { createRoute } from "@btst/yar";
6
7
  import type { QueryClient } from "@tanstack/react-query";
@@ -226,6 +227,12 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
226
227
  } catch (error) {
227
228
  // Error hook - log the error but don't throw during SSR
228
229
  // Let Error Boundaries handle errors when components render
230
+ if (isConnectionError(error)) {
231
+ console.warn(
232
+ "[btst/blog] route.loader() failed — no server running at build time. " +
233
+ "Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
234
+ );
235
+ }
229
236
  if (hooks?.onLoadError) {
230
237
  await hooks.onLoadError(error as Error, context);
231
238
  }
@@ -299,6 +306,12 @@ function createPostLoader(
299
306
  } catch (error) {
300
307
  // Error hook - log the error but don't throw during SSR
301
308
  // Let Error Boundaries handle errors when components render
309
+ if (isConnectionError(error)) {
310
+ console.warn(
311
+ "[btst/blog] route.loader() failed — no server running at build time. " +
312
+ "Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
313
+ );
314
+ }
302
315
  if (hooks?.onLoadError) {
303
316
  await hooks.onLoadError(error as Error, context);
304
317
  }
@@ -398,6 +411,12 @@ function createTagLoader(tagSlug: string, config: BlogClientConfig) {
398
411
  await hooks.onLoadError(error, context);
399
412
  }
400
413
  } catch (error) {
414
+ if (isConnectionError(error)) {
415
+ console.warn(
416
+ "[btst/blog] route.loader() failed — no server running at build time. " +
417
+ "Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
418
+ );
419
+ }
401
420
  if (hooks?.onLoadError) {
402
421
  await hooks.onLoadError(error as Error, context);
403
422
  }