@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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import type { Post, PostWithPostTag, Tag } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parameters for filtering/paginating posts.
|
|
6
|
+
* Mirrors the shape of the list API query schema.
|
|
7
|
+
*/
|
|
8
|
+
export interface PostListParams {
|
|
9
|
+
slug?: string;
|
|
10
|
+
tagSlug?: string;
|
|
11
|
+
offset?: number;
|
|
12
|
+
limit?: number;
|
|
13
|
+
query?: string;
|
|
14
|
+
published?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Paginated result returned by {@link getAllPosts}.
|
|
19
|
+
*/
|
|
20
|
+
export interface PostListResult {
|
|
21
|
+
items: Array<Post & { tags: Tag[] }>;
|
|
22
|
+
total: number;
|
|
23
|
+
limit?: number;
|
|
24
|
+
offset?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Retrieve all posts matching optional filter criteria.
|
|
29
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
30
|
+
*
|
|
31
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeListPosts`) are NOT
|
|
32
|
+
* called. The caller is responsible for any access-control checks before
|
|
33
|
+
* invoking this function.
|
|
34
|
+
*
|
|
35
|
+
* @param adapter - The database adapter
|
|
36
|
+
* @param params - Optional filter/pagination parameters (same shape as the list API query)
|
|
37
|
+
*/
|
|
38
|
+
export async function getAllPosts(
|
|
39
|
+
adapter: Adapter,
|
|
40
|
+
params?: PostListParams,
|
|
41
|
+
): Promise<PostListResult> {
|
|
42
|
+
const query = params ?? {};
|
|
43
|
+
|
|
44
|
+
const whereConditions: Array<{
|
|
45
|
+
field: string;
|
|
46
|
+
value: string | number | boolean | string[] | number[] | Date | null;
|
|
47
|
+
operator: "eq" | "in";
|
|
48
|
+
}> = [];
|
|
49
|
+
|
|
50
|
+
// Resolve tagSlug → post IDs up front, then push an `in` condition so the
|
|
51
|
+
// adapter can filter and paginate entirely at the DB level. The previous
|
|
52
|
+
// approach loaded every post into memory and filtered with a JS Set, which
|
|
53
|
+
// scans the whole table on every request.
|
|
54
|
+
if (query.tagSlug) {
|
|
55
|
+
const tag = await adapter.findOne<Tag>({
|
|
56
|
+
model: "tag",
|
|
57
|
+
where: [{ field: "slug", value: query.tagSlug, operator: "eq" as const }],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!tag) {
|
|
61
|
+
return { items: [], total: 0, limit: query.limit, offset: query.offset };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const postTags = await adapter.findMany<{ postId: string; tagId: string }>({
|
|
65
|
+
model: "postTag",
|
|
66
|
+
where: [{ field: "tagId", value: tag.id, operator: "eq" as const }],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const taggedPostIds = postTags.map((pt) => pt.postId);
|
|
70
|
+
if (taggedPostIds.length === 0) {
|
|
71
|
+
return { items: [], total: 0, limit: query.limit, offset: query.offset };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
whereConditions.push({
|
|
75
|
+
field: "id",
|
|
76
|
+
value: taggedPostIds,
|
|
77
|
+
operator: "in" as const,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (query.published !== undefined) {
|
|
82
|
+
whereConditions.push({
|
|
83
|
+
field: "published",
|
|
84
|
+
value: query.published,
|
|
85
|
+
operator: "eq" as const,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (query.slug) {
|
|
90
|
+
whereConditions.push({
|
|
91
|
+
field: "slug",
|
|
92
|
+
value: query.slug,
|
|
93
|
+
operator: "eq" as const,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Full-text search across title/content/excerpt must remain in-memory:
|
|
98
|
+
// the adapter's `contains` operator is case-sensitive and cannot be
|
|
99
|
+
// grouped with AND conditions using OR connectors in all adapter
|
|
100
|
+
// implementations. All other filters above are pushed to DB, so the
|
|
101
|
+
// in-memory pass only scans the already-narrowed result set.
|
|
102
|
+
const needsInMemoryFilter = !!query.query;
|
|
103
|
+
|
|
104
|
+
const dbWhere = whereConditions.length > 0 ? whereConditions : undefined;
|
|
105
|
+
|
|
106
|
+
const dbTotal: number | undefined = !needsInMemoryFilter
|
|
107
|
+
? await adapter.count({ model: "post", where: dbWhere })
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
110
|
+
const posts = await adapter.findMany<PostWithPostTag>({
|
|
111
|
+
model: "post",
|
|
112
|
+
limit: !needsInMemoryFilter ? query.limit : undefined,
|
|
113
|
+
offset: !needsInMemoryFilter ? query.offset : undefined,
|
|
114
|
+
where: dbWhere,
|
|
115
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
116
|
+
join: { postTag: true },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Collect the unique tag IDs present in this page of posts, then fetch
|
|
120
|
+
// only those tags (not the entire tags table).
|
|
121
|
+
const tagIds = new Set<string>();
|
|
122
|
+
for (const post of posts) {
|
|
123
|
+
if (post.postTag) {
|
|
124
|
+
for (const pt of post.postTag) {
|
|
125
|
+
tagIds.add(pt.tagId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const tags =
|
|
131
|
+
tagIds.size > 0
|
|
132
|
+
? await adapter.findMany<Tag>({
|
|
133
|
+
model: "tag",
|
|
134
|
+
where: [
|
|
135
|
+
{
|
|
136
|
+
field: "id",
|
|
137
|
+
value: Array.from(tagIds),
|
|
138
|
+
operator: "in" as const,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
})
|
|
142
|
+
: [];
|
|
143
|
+
const tagMap = new Map<string, Tag>(tags.map((t) => [t.id, t]));
|
|
144
|
+
|
|
145
|
+
let result = posts.map((post) => {
|
|
146
|
+
const postTags = (post.postTag || [])
|
|
147
|
+
.map((pt) => {
|
|
148
|
+
const tag = tagMap.get(pt.tagId);
|
|
149
|
+
return tag ? { ...tag } : undefined;
|
|
150
|
+
})
|
|
151
|
+
.filter((tag): tag is Tag => tag !== undefined);
|
|
152
|
+
const { postTag: _, ...postWithoutJoin } = post;
|
|
153
|
+
return { ...postWithoutJoin, tags: postTags };
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (query.query) {
|
|
157
|
+
const searchLower = query.query.toLowerCase();
|
|
158
|
+
result = result.filter(
|
|
159
|
+
(post) =>
|
|
160
|
+
post.title?.toLowerCase().includes(searchLower) ||
|
|
161
|
+
post.content?.toLowerCase().includes(searchLower) ||
|
|
162
|
+
post.excerpt?.toLowerCase().includes(searchLower),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (needsInMemoryFilter) {
|
|
167
|
+
const total = result.length;
|
|
168
|
+
const offset = query.offset ?? 0;
|
|
169
|
+
const limit = query.limit;
|
|
170
|
+
result = result.slice(
|
|
171
|
+
offset,
|
|
172
|
+
limit !== undefined ? offset + limit : undefined,
|
|
173
|
+
);
|
|
174
|
+
return { items: result, total, limit: query.limit, offset: query.offset };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
items: result,
|
|
179
|
+
total: dbTotal ?? result.length,
|
|
180
|
+
limit: query.limit,
|
|
181
|
+
offset: query.offset,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Retrieve a single post by its slug, including associated tags.
|
|
187
|
+
* Returns null if no post is found.
|
|
188
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
189
|
+
*
|
|
190
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
191
|
+
* responsible for any access-control checks before invoking this function.
|
|
192
|
+
*
|
|
193
|
+
* @param adapter - The database adapter
|
|
194
|
+
* @param slug - The post slug
|
|
195
|
+
*/
|
|
196
|
+
export async function getPostBySlug(
|
|
197
|
+
adapter: Adapter,
|
|
198
|
+
slug: string,
|
|
199
|
+
): Promise<(Post & { tags: Tag[] }) | null> {
|
|
200
|
+
const posts = await adapter.findMany<PostWithPostTag>({
|
|
201
|
+
model: "post",
|
|
202
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
203
|
+
limit: 1,
|
|
204
|
+
join: { postTag: true },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (posts.length === 0) return null;
|
|
208
|
+
|
|
209
|
+
const post = posts[0]!;
|
|
210
|
+
const tagIds = (post.postTag || []).map((pt) => pt.tagId);
|
|
211
|
+
|
|
212
|
+
const tags =
|
|
213
|
+
tagIds.length > 0
|
|
214
|
+
? await adapter.findMany<Tag>({
|
|
215
|
+
model: "tag",
|
|
216
|
+
where: [{ field: "id", value: tagIds, operator: "in" as const }],
|
|
217
|
+
})
|
|
218
|
+
: [];
|
|
219
|
+
|
|
220
|
+
const tagMap = new Map<string, Tag>(tags.map((t) => [t.id, t]));
|
|
221
|
+
const resolvedTags = (post.postTag || [])
|
|
222
|
+
.map((pt) => tagMap.get(pt.tagId))
|
|
223
|
+
.filter((t): t is Tag => t !== undefined);
|
|
224
|
+
|
|
225
|
+
const { postTag: _, ...postWithoutJoin } = post;
|
|
226
|
+
return { ...postWithoutJoin, tags: resolvedTags };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Retrieve all tags, sorted alphabetically by name.
|
|
231
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
232
|
+
*
|
|
233
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
234
|
+
* responsible for any access-control checks before invoking this function.
|
|
235
|
+
*
|
|
236
|
+
* @param adapter - The database adapter
|
|
237
|
+
*/
|
|
238
|
+
export async function getAllTags(adapter: Adapter): Promise<Tag[]> {
|
|
239
|
+
return adapter.findMany<Tag>({
|
|
240
|
+
model: "tag",
|
|
241
|
+
sortBy: { field: "name", direction: "asc" },
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
export * from "./plugin";
|
|
2
|
+
export {
|
|
3
|
+
getAllPosts,
|
|
4
|
+
getPostBySlug,
|
|
5
|
+
getAllTags,
|
|
6
|
+
type PostListParams,
|
|
7
|
+
type PostListResult,
|
|
8
|
+
} from "./getters";
|
|
9
|
+
export { serializePost, serializeTag } from "./serializers";
|
|
10
|
+
export { BLOG_QUERY_KEYS } from "./query-key-defs";
|
|
2
11
|
export { createBlogQueryKeys } from "../query-keys";
|
|
@@ -6,6 +6,91 @@ import { blogSchema as dbSchema } from "../db";
|
|
|
6
6
|
import type { Post, PostWithPostTag, Tag } from "../types";
|
|
7
7
|
import { slugify } from "../utils";
|
|
8
8
|
import { createPostSchema, updatePostSchema } from "../schemas";
|
|
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
|
+
}
|
|
9
94
|
|
|
10
95
|
export const PostListQuerySchema = z.object({
|
|
11
96
|
slug: z.string().optional(),
|
|
@@ -86,12 +171,12 @@ export interface BlogBackendHooks {
|
|
|
86
171
|
|
|
87
172
|
/**
|
|
88
173
|
* Called after posts are read successfully
|
|
89
|
-
* @param posts -
|
|
174
|
+
* @param posts - The list of posts returned by the query
|
|
90
175
|
* @param filter - Query parameters used for filtering
|
|
91
176
|
* @param context - Request context
|
|
92
177
|
*/
|
|
93
178
|
onPostsRead?: (
|
|
94
|
-
posts: Post[]
|
|
179
|
+
posts: Array<Post & { tags: Tag[] }>,
|
|
95
180
|
filter: z.infer<typeof PostListQuerySchema>,
|
|
96
181
|
context: BlogApiContext,
|
|
97
182
|
) => Promise<void> | void;
|
|
@@ -168,6 +253,14 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
168
253
|
|
|
169
254
|
dbPlugin: dbSchema,
|
|
170
255
|
|
|
256
|
+
api: (adapter) => ({
|
|
257
|
+
getAllPosts: (params?: Parameters<typeof getAllPosts>[1]) =>
|
|
258
|
+
getAllPosts(adapter, params),
|
|
259
|
+
getPostBySlug: (slug: string) => getPostBySlug(adapter, slug),
|
|
260
|
+
getAllTags: () => getAllTags(adapter),
|
|
261
|
+
prefetchForRoute: createBlogPrefetchForRoute(adapter),
|
|
262
|
+
}),
|
|
263
|
+
|
|
171
264
|
routes: (adapter: Adapter) => {
|
|
172
265
|
const findOrCreateTags = async (
|
|
173
266
|
tagInputs: Array<
|
|
@@ -265,144 +358,10 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
265
358
|
}
|
|
266
359
|
}
|
|
267
360
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (query.tagSlug) {
|
|
271
|
-
const tag = await adapter.findOne<Tag>({
|
|
272
|
-
model: "tag",
|
|
273
|
-
where: [
|
|
274
|
-
{
|
|
275
|
-
field: "slug",
|
|
276
|
-
value: query.tagSlug,
|
|
277
|
-
operator: "eq" as const,
|
|
278
|
-
},
|
|
279
|
-
],
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
if (!tag) {
|
|
283
|
-
return [];
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const postTags = await adapter.findMany<{
|
|
287
|
-
postId: string;
|
|
288
|
-
tagId: string;
|
|
289
|
-
}>({
|
|
290
|
-
model: "postTag",
|
|
291
|
-
where: [
|
|
292
|
-
{
|
|
293
|
-
field: "tagId",
|
|
294
|
-
value: tag.id,
|
|
295
|
-
operator: "eq" as const,
|
|
296
|
-
},
|
|
297
|
-
],
|
|
298
|
-
});
|
|
299
|
-
tagFilterPostIds = new Set(postTags.map((pt) => pt.postId));
|
|
300
|
-
if (tagFilterPostIds.size === 0) {
|
|
301
|
-
return [];
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const whereConditions = [];
|
|
306
|
-
|
|
307
|
-
if (query.published !== undefined) {
|
|
308
|
-
whereConditions.push({
|
|
309
|
-
field: "published",
|
|
310
|
-
value: query.published,
|
|
311
|
-
operator: "eq" as const,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (query.slug) {
|
|
316
|
-
whereConditions.push({
|
|
317
|
-
field: "slug",
|
|
318
|
-
value: query.slug,
|
|
319
|
-
operator: "eq" as const,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const posts = await adapter.findMany<PostWithPostTag>({
|
|
324
|
-
model: "post",
|
|
325
|
-
limit:
|
|
326
|
-
query.query || query.tagSlug ? undefined : (query.limit ?? 10),
|
|
327
|
-
offset:
|
|
328
|
-
query.query || query.tagSlug ? undefined : (query.offset ?? 0),
|
|
329
|
-
where: whereConditions,
|
|
330
|
-
sortBy: {
|
|
331
|
-
field: "createdAt",
|
|
332
|
-
direction: "desc",
|
|
333
|
-
},
|
|
334
|
-
join: {
|
|
335
|
-
postTag: true,
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
// Collect unique tag IDs from joined postTag data
|
|
340
|
-
const tagIds = new Set<string>();
|
|
341
|
-
for (const post of posts) {
|
|
342
|
-
if (post.postTag) {
|
|
343
|
-
for (const pt of post.postTag) {
|
|
344
|
-
tagIds.add(pt.tagId);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Fetch all tags at once
|
|
350
|
-
const tags =
|
|
351
|
-
tagIds.size > 0
|
|
352
|
-
? await adapter.findMany<Tag>({
|
|
353
|
-
model: "tag",
|
|
354
|
-
})
|
|
355
|
-
: [];
|
|
356
|
-
const tagMap = new Map<string, Tag>();
|
|
357
|
-
for (const tag of tags) {
|
|
358
|
-
if (tagIds.has(tag.id)) {
|
|
359
|
-
tagMap.set(tag.id, tag);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Map tags to posts (spread to avoid circular references)
|
|
364
|
-
let result = posts.map((post) => {
|
|
365
|
-
const postTags = (post.postTag || [])
|
|
366
|
-
.map((pt) => {
|
|
367
|
-
const tag = tagMap.get(pt.tagId);
|
|
368
|
-
return tag ? { ...tag } : undefined;
|
|
369
|
-
})
|
|
370
|
-
.filter((tag): tag is Tag => tag !== undefined);
|
|
371
|
-
const { postTag: _, ...postWithoutJoin } = post;
|
|
372
|
-
return {
|
|
373
|
-
...postWithoutJoin,
|
|
374
|
-
tags: postTags,
|
|
375
|
-
};
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
if (tagFilterPostIds) {
|
|
379
|
-
result = result.filter((post) => tagFilterPostIds!.has(post.id));
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (query.query) {
|
|
383
|
-
const searchLower = query.query.toLowerCase();
|
|
384
|
-
result = result.filter((post) => {
|
|
385
|
-
const titleMatch = post.title
|
|
386
|
-
?.toLowerCase()
|
|
387
|
-
.includes(searchLower);
|
|
388
|
-
const contentMatch = post.content
|
|
389
|
-
?.toLowerCase()
|
|
390
|
-
.includes(searchLower);
|
|
391
|
-
const excerptMatch = post.excerpt
|
|
392
|
-
?.toLowerCase()
|
|
393
|
-
.includes(searchLower);
|
|
394
|
-
return titleMatch || contentMatch || excerptMatch;
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (query.tagSlug || query.query) {
|
|
399
|
-
const offset = query.offset ?? 0;
|
|
400
|
-
const limit = query.limit ?? 10;
|
|
401
|
-
result = result.slice(offset, offset + limit);
|
|
402
|
-
}
|
|
361
|
+
const result = await getAllPosts(adapter, query);
|
|
403
362
|
|
|
404
363
|
if (hooks?.onPostsRead) {
|
|
405
|
-
await hooks.onPostsRead(result, query, context);
|
|
364
|
+
await hooks.onPostsRead(result.items, query, context);
|
|
406
365
|
}
|
|
407
366
|
|
|
408
367
|
return result;
|
|
@@ -806,9 +765,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
806
765
|
method: "GET",
|
|
807
766
|
},
|
|
808
767
|
async () => {
|
|
809
|
-
return await adapter
|
|
810
|
-
model: "tag",
|
|
811
|
-
});
|
|
768
|
+
return await getAllTags(adapter);
|
|
812
769
|
},
|
|
813
770
|
);
|
|
814
771
|
|
|
@@ -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
|
}
|
|
@@ -750,7 +769,8 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
|
|
|
750
769
|
published: "true",
|
|
751
770
|
},
|
|
752
771
|
});
|
|
753
|
-
|
|
772
|
+
// The /posts endpoint returns PostListResult { items, total, limit, offset }
|
|
773
|
+
const page = ((res.data as any)?.items ?? []) as SerializedPost[];
|
|
754
774
|
posts.push(...page);
|
|
755
775
|
if (page.length < limit) break;
|
|
756
776
|
offset += limit;
|