@btst/stack 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.cjs +9 -1
- package/dist/api/index.d.cts +4 -4
- package/dist/api/index.d.mts +4 -4
- package/dist/api/index.d.ts +4 -4
- package/dist/api/index.mjs +9 -1
- package/dist/client/index.d.cts +2 -2
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +42 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +39 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
- package/dist/packages/stack/src/plugins/blog/api/getters.cjs +131 -0
- package/dist/packages/stack/src/plugins/blog/api/getters.mjs +127 -0
- package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +9 -107
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +9 -107
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +1 -1
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +1 -1
- package/dist/packages/stack/src/plugins/cms/api/getters.cjs +146 -0
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +138 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +560 -622
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +559 -621
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +1 -1
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +1 -1
- package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +6 -3
- package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +6 -3
- package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +111 -0
- package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +104 -0
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +16 -88
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +12 -84
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +1 -1
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +84 -0
- package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +81 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +9 -123
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +9 -123
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +1 -1
- package/dist/plugins/ai-chat/api/index.cjs +3 -0
- package/dist/plugins/ai-chat/api/index.d.cts +27 -4
- package/dist/plugins/ai-chat/api/index.d.mts +27 -4
- package/dist/plugins/ai-chat/api/index.d.ts +27 -4
- package/dist/plugins/ai-chat/api/index.mjs +1 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
- package/dist/plugins/ai-chat/query-keys.d.cts +9 -284
- package/dist/plugins/ai-chat/query-keys.d.mts +9 -284
- package/dist/plugins/ai-chat/query-keys.d.ts +9 -284
- package/dist/plugins/api/index.d.cts +4 -3
- package/dist/plugins/api/index.d.mts +4 -3
- package/dist/plugins/api/index.d.ts +4 -3
- package/dist/plugins/blog/api/index.cjs +4 -0
- package/dist/plugins/blog/api/index.d.cts +3 -2
- package/dist/plugins/blog/api/index.d.mts +3 -2
- package/dist/plugins/blog/api/index.d.ts +3 -2
- package/dist/plugins/blog/api/index.mjs +1 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +4 -4
- package/dist/plugins/blog/client/hooks/index.d.mts +4 -4
- package/dist/plugins/blog/client/hooks/index.d.ts +4 -4
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.cjs +7 -4
- package/dist/plugins/blog/query-keys.d.cts +81 -27
- package/dist/plugins/blog/query-keys.d.mts +81 -27
- package/dist/plugins/blog/query-keys.d.ts +81 -27
- package/dist/plugins/blog/query-keys.mjs +7 -4
- package/dist/plugins/client/index.d.cts +2 -2
- package/dist/plugins/client/index.d.mts +2 -2
- package/dist/plugins/client/index.d.ts +2 -2
- package/dist/plugins/cms/api/index.cjs +4 -0
- package/dist/plugins/cms/api/index.d.cts +61 -5
- package/dist/plugins/cms/api/index.d.mts +61 -5
- package/dist/plugins/cms/api/index.d.ts +61 -5
- package/dist/plugins/cms/api/index.mjs +1 -0
- package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.d.cts +2 -1
- package/dist/plugins/cms/query-keys.d.mts +2 -1
- package/dist/plugins/cms/query-keys.d.ts +2 -1
- package/dist/plugins/form-builder/api/index.cjs +4 -0
- package/dist/plugins/form-builder/api/index.d.cts +77 -7
- package/dist/plugins/form-builder/api/index.d.mts +77 -7
- package/dist/plugins/form-builder/api/index.d.ts +77 -7
- package/dist/plugins/form-builder/api/index.mjs +1 -0
- package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.d.cts +2 -1
- package/dist/plugins/form-builder/query-keys.d.mts +2 -1
- package/dist/plugins/form-builder/query-keys.d.ts +2 -1
- package/dist/plugins/kanban/api/index.cjs +3 -0
- package/dist/plugins/kanban/api/index.d.cts +40 -43
- package/dist/plugins/kanban/api/index.d.mts +40 -43
- package/dist/plugins/kanban/api/index.d.ts +40 -43
- package/dist/plugins/kanban/api/index.mjs +1 -0
- package/dist/plugins/kanban/client/components/index.d.cts +1 -1
- package/dist/plugins/kanban/client/components/index.d.mts +1 -1
- package/dist/plugins/kanban/client/components/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.cjs +4 -3
- package/dist/plugins/kanban/query-keys.d.cts +2 -1
- package/dist/plugins/kanban/query-keys.d.mts +2 -1
- package/dist/plugins/kanban/query-keys.d.ts +2 -1
- package/dist/plugins/kanban/query-keys.mjs +4 -3
- package/dist/plugins/open-api/api/index.d.cts +2 -2
- package/dist/plugins/open-api/api/index.d.mts +2 -2
- package/dist/plugins/open-api/api/index.d.ts +2 -2
- package/dist/plugins/route-docs/client/index.d.cts +1 -1
- package/dist/plugins/route-docs/client/index.d.mts +1 -1
- package/dist/plugins/route-docs/client/index.d.ts +1 -1
- package/dist/plugins/ui-builder/index.d.cts +1 -1
- package/dist/plugins/ui-builder/index.d.mts +1 -1
- package/dist/plugins/ui-builder/index.d.ts +1 -1
- package/dist/shared/{stack.BoA0xkJv.d.cts → stack.7n9Y_u7N.d.cts} +33 -7
- package/dist/shared/{stack.BoA0xkJv.d.mts → stack.7n9Y_u7N.d.mts} +33 -7
- package/dist/shared/{stack.BoA0xkJv.d.ts → stack.7n9Y_u7N.d.ts} +33 -7
- package/dist/shared/stack.BeSm90va.d.ts +289 -0
- package/dist/shared/{stack.DzH_wcvr.d.mts → stack.CIrIsc-A.d.cts} +2 -2
- package/dist/shared/{stack.DzH_wcvr.d.ts → stack.CIrIsc-A.d.mts} +2 -2
- package/dist/shared/{stack.DzH_wcvr.d.cts → stack.CIrIsc-A.d.ts} +2 -2
- package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
- package/dist/shared/{stack.BsXokfNh.d.mts → stack.CXjzTMsb.d.cts} +1 -1
- package/dist/shared/{stack.BsXokfNh.d.ts → stack.CXjzTMsb.d.mts} +1 -1
- package/dist/shared/{stack.BsXokfNh.d.cts → stack.CXjzTMsb.d.ts} +1 -1
- package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
- package/dist/shared/{stack.DKDMI-QO.d.mts → stack.QD1y_7NY.d.cts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.ts → stack.QD1y_7NY.d.mts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.cts → stack.QD1y_7NY.d.ts} +7 -1
- package/package.json +1 -1
- package/src/__tests__/stack-api.test.ts +118 -0
- package/src/api/index.ts +15 -1
- package/src/plugins/ai-chat/__tests__/getters.test.ts +109 -0
- package/src/plugins/ai-chat/api/getters.ts +71 -0
- package/src/plugins/ai-chat/api/index.ts +1 -0
- package/src/plugins/ai-chat/api/plugin.ts +8 -0
- package/src/plugins/api/index.ts +3 -1
- package/src/plugins/blog/__tests__/getters.test.ts +540 -0
- package/src/plugins/blog/api/getters.ts +243 -0
- package/src/plugins/blog/api/index.ts +7 -0
- package/src/plugins/blog/api/plugin.ts +13 -141
- package/src/plugins/blog/client/plugin.tsx +2 -1
- package/src/plugins/blog/query-keys.ts +16 -13
- package/src/plugins/cms/__tests__/getters.test.ts +206 -0
- package/src/plugins/cms/api/getters.ts +244 -0
- package/src/plugins/cms/api/index.ts +5 -0
- package/src/plugins/cms/api/plugin.ts +50 -154
- package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +1 -1
- package/src/plugins/cms/client/hooks/cms-hooks.tsx +3 -0
- package/src/plugins/cms/types.ts +1 -1
- package/src/plugins/form-builder/__tests__/getters.test.ts +159 -0
- package/src/plugins/form-builder/api/getters.ts +203 -0
- package/src/plugins/form-builder/api/index.ts +1 -0
- package/src/plugins/form-builder/api/plugin.ts +22 -115
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
- package/src/plugins/form-builder/types.ts +2 -2
- package/src/plugins/kanban/__tests__/getters.test.ts +172 -0
- package/src/plugins/kanban/api/getters.ts +149 -0
- package/src/plugins/kanban/api/index.ts +1 -0
- package/src/plugins/kanban/api/plugin.ts +16 -146
- package/src/plugins/kanban/client/plugin.tsx +2 -1
- package/src/plugins/kanban/query-keys.ts +8 -5
- package/src/types.ts +44 -5
- package/dist/shared/{stack.CbuN2zVV.d.cts → stack.BkYlUT_8.d.cts} +6 -6
- package/dist/shared/{stack.CbuN2zVV.d.mts → stack.BkYlUT_8.d.mts} +6 -6
- package/dist/shared/{stack.CbuN2zVV.d.ts → stack.BkYlUT_8.d.ts} +6 -6
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -6,6 +6,7 @@ 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";
|
|
9
10
|
|
|
10
11
|
export const PostListQuerySchema = z.object({
|
|
11
12
|
slug: z.string().optional(),
|
|
@@ -86,12 +87,12 @@ export interface BlogBackendHooks {
|
|
|
86
87
|
|
|
87
88
|
/**
|
|
88
89
|
* Called after posts are read successfully
|
|
89
|
-
* @param posts -
|
|
90
|
+
* @param posts - The list of posts returned by the query
|
|
90
91
|
* @param filter - Query parameters used for filtering
|
|
91
92
|
* @param context - Request context
|
|
92
93
|
*/
|
|
93
94
|
onPostsRead?: (
|
|
94
|
-
posts: Post[]
|
|
95
|
+
posts: Array<Post & { tags: Tag[] }>,
|
|
95
96
|
filter: z.infer<typeof PostListQuerySchema>,
|
|
96
97
|
context: BlogApiContext,
|
|
97
98
|
) => Promise<void> | void;
|
|
@@ -168,6 +169,13 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
168
169
|
|
|
169
170
|
dbPlugin: dbSchema,
|
|
170
171
|
|
|
172
|
+
api: (adapter) => ({
|
|
173
|
+
getAllPosts: (params?: Parameters<typeof getAllPosts>[1]) =>
|
|
174
|
+
getAllPosts(adapter, params),
|
|
175
|
+
getPostBySlug: (slug: string) => getPostBySlug(adapter, slug),
|
|
176
|
+
getAllTags: () => getAllTags(adapter),
|
|
177
|
+
}),
|
|
178
|
+
|
|
171
179
|
routes: (adapter: Adapter) => {
|
|
172
180
|
const findOrCreateTags = async (
|
|
173
181
|
tagInputs: Array<
|
|
@@ -265,144 +273,10 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
265
273
|
}
|
|
266
274
|
}
|
|
267
275
|
|
|
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
|
-
}
|
|
276
|
+
const result = await getAllPosts(adapter, query);
|
|
403
277
|
|
|
404
278
|
if (hooks?.onPostsRead) {
|
|
405
|
-
await hooks.onPostsRead(result, query, context);
|
|
279
|
+
await hooks.onPostsRead(result.items, query, context);
|
|
406
280
|
}
|
|
407
281
|
|
|
408
282
|
return result;
|
|
@@ -806,9 +680,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
806
680
|
method: "GET",
|
|
807
681
|
},
|
|
808
682
|
async () => {
|
|
809
|
-
return await adapter
|
|
810
|
-
model: "tag",
|
|
811
|
-
});
|
|
683
|
+
return await getAllTags(adapter);
|
|
812
684
|
},
|
|
813
685
|
);
|
|
814
686
|
|
|
@@ -750,7 +750,8 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
|
|
|
750
750
|
published: "true",
|
|
751
751
|
},
|
|
752
752
|
});
|
|
753
|
-
|
|
753
|
+
// The /posts endpoint returns PostListResult { items, total, limit, offset }
|
|
754
|
+
const page = ((res.data as any)?.items ?? []) as SerializedPost[];
|
|
754
755
|
posts.push(...page);
|
|
755
756
|
if (page.length < limit) break;
|
|
756
757
|
offset += limit;
|
|
@@ -99,13 +99,14 @@ function createPostsQueries(
|
|
|
99
99
|
},
|
|
100
100
|
headers,
|
|
101
101
|
});
|
|
102
|
-
// Check for errors (better-call returns Error$1<unknown> | Data<
|
|
102
|
+
// Check for errors (better-call returns Error$1<unknown> | Data<PostListResult>)
|
|
103
103
|
if (isErrorResponse(response)) {
|
|
104
104
|
const errorResponse = response as { error: unknown };
|
|
105
105
|
throw toError(errorResponse.error);
|
|
106
106
|
}
|
|
107
|
-
//
|
|
108
|
-
|
|
107
|
+
// Extract .items from the paginated response for infinite scroll compatibility
|
|
108
|
+
const dataResponse = response as { data?: { items?: unknown[] } };
|
|
109
|
+
return (dataResponse.data?.items ??
|
|
109
110
|
[]) as unknown as SerializedPost[];
|
|
110
111
|
} catch (error) {
|
|
111
112
|
// Re-throw errors so React Query can catch them
|
|
@@ -126,14 +127,14 @@ function createPostsQueries(
|
|
|
126
127
|
query: { slug, limit: 1 },
|
|
127
128
|
headers,
|
|
128
129
|
});
|
|
129
|
-
// Check for errors (better-call returns Error$1<unknown> | Data<
|
|
130
|
+
// Check for errors (better-call returns Error$1<unknown> | Data<PostListResult>)
|
|
130
131
|
if (isErrorResponse(response)) {
|
|
131
132
|
const errorResponse = response as { error: unknown };
|
|
132
133
|
throw toError(errorResponse.error);
|
|
133
134
|
}
|
|
134
|
-
// Type narrowed to Data<
|
|
135
|
-
const dataResponse = response as { data?: unknown[] };
|
|
136
|
-
return (dataResponse.data?.[0] ??
|
|
135
|
+
// Type narrowed to Data<PostListResult> after error check — access .items[0]
|
|
136
|
+
const dataResponse = response as { data?: { items?: unknown[] } };
|
|
137
|
+
return (dataResponse.data?.items?.[0] ??
|
|
137
138
|
null) as unknown as SerializedPost | null;
|
|
138
139
|
} catch (error) {
|
|
139
140
|
// Re-throw errors so React Query can catch them
|
|
@@ -181,13 +182,14 @@ function createPostsQueries(
|
|
|
181
182
|
},
|
|
182
183
|
headers,
|
|
183
184
|
});
|
|
184
|
-
// Check for errors (better-call returns Error$1<unknown> | Data<
|
|
185
|
+
// Check for errors (better-call returns Error$1<unknown> | Data<PostListResult>)
|
|
185
186
|
if (isErrorResponse(response)) {
|
|
186
187
|
const errorResponse = response as { error: unknown };
|
|
187
188
|
throw toError(errorResponse.error);
|
|
188
189
|
}
|
|
189
|
-
//
|
|
190
|
-
|
|
190
|
+
// Extract .items from the paginated response
|
|
191
|
+
const recentResponse = response as { data?: { items?: unknown[] } };
|
|
192
|
+
let posts = (recentResponse.data?.items ??
|
|
191
193
|
[]) as unknown as SerializedPost[];
|
|
192
194
|
|
|
193
195
|
// Exclude current post if specified
|
|
@@ -228,13 +230,14 @@ function createDraftsQueries(
|
|
|
228
230
|
},
|
|
229
231
|
headers,
|
|
230
232
|
});
|
|
231
|
-
// Check for errors (better-call returns Error$1<unknown> | Data<
|
|
233
|
+
// Check for errors (better-call returns Error$1<unknown> | Data<PostListResult>)
|
|
232
234
|
if (isErrorResponse(response)) {
|
|
233
235
|
const errorResponse = response as { error: unknown };
|
|
234
236
|
throw toError(errorResponse.error);
|
|
235
237
|
}
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
+
// Extract .items from the paginated response for infinite scroll compatibility
|
|
239
|
+
const draftsResponse = response as { data?: { items?: unknown[] } };
|
|
240
|
+
return (draftsResponse.data?.items ??
|
|
238
241
|
[]) as unknown as SerializedPost[];
|
|
239
242
|
} catch (error) {
|
|
240
243
|
// Re-throw errors so React Query can catch them
|