@btst/stack 2.11.1 → 2.11.2
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/packages/stack/src/plugins/blog/api/mutations.cjs +170 -0
- package/dist/packages/stack/src/plugins/blog/api/mutations.mjs +166 -0
- package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +34 -157
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +40 -163
- package/dist/plugins/blog/api/index.cjs +4 -0
- package/dist/plugins/blog/api/index.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/api/index.mjs +1 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +2 -2
- package/dist/plugins/blog/client/index.d.mts +2 -2
- package/dist/plugins/blog/client/index.d.ts +2 -2
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/shared/{stack.DvMUCTTl.d.mts → stack.D0p6oNme.d.ts} +96 -13
- package/dist/shared/{stack.DGsFF5qb.d.cts → stack.DOZ1EXjM.d.mts} +5 -5
- package/dist/shared/{stack.qta0-CPq.d.ts → stack.DWipT53I.d.cts} +96 -13
- package/dist/shared/{stack.NtflNnnH.d.mts → stack.DX-tQ93o.d.cts} +5 -5
- package/dist/shared/{stack.DEW8EtFu.d.cts → stack.E17kSK1W.d.mts} +96 -13
- package/dist/shared/{stack.QrBE0Bc8.d.ts → stack.VF6FhyZw.d.ts} +5 -5
- package/package.json +1 -1
- package/src/plugins/blog/api/index.ts +7 -0
- package/src/plugins/blog/api/mutations.ts +287 -0
- package/src/plugins/blog/api/plugin.ts +43 -184
- package/dist/shared/{stack.BOokfhZD.d.cts → stack.B6S3cgwN.d.cts} +16 -16
- package/dist/shared/{stack.Sod_PZhB.d.cts → stack.BWp0hcm9.d.cts} +7 -7
- package/dist/shared/{stack.Sod_PZhB.d.mts → stack.BWp0hcm9.d.mts} +7 -7
- package/dist/shared/{stack.Sod_PZhB.d.ts → stack.BWp0hcm9.d.ts} +7 -7
- package/dist/shared/{stack.CWxAl9K3.d.mts → stack.Bzfx-_lq.d.mts} +16 -16
- package/dist/shared/{stack.BvCR4-9H.d.ts → stack.j5SFLC1d.d.ts} +16 -16
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { DBAdapter as Adapter } from "@btst/db";
|
|
2
|
+
import type { Post, Tag } from "../types";
|
|
3
|
+
import { slugify } from "../utils";
|
|
4
|
+
|
|
5
|
+
type TagInput = { name: string } | { id: string; name: string; slug: string };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find existing tags by slug or create missing ones, then return the resolved Tag records.
|
|
9
|
+
* Tags that already carry an `id` are returned as-is (after name normalisation).
|
|
10
|
+
*/
|
|
11
|
+
async function findOrCreateTags(
|
|
12
|
+
adapter: Adapter,
|
|
13
|
+
tagInputs: TagInput[],
|
|
14
|
+
): Promise<Tag[]> {
|
|
15
|
+
if (tagInputs.length === 0) return [];
|
|
16
|
+
|
|
17
|
+
const normalizeTagName = (name: string): string => name.trim();
|
|
18
|
+
|
|
19
|
+
const tagsWithIds: Tag[] = [];
|
|
20
|
+
const tagsToFindOrCreate: Array<{ name: string }> = [];
|
|
21
|
+
|
|
22
|
+
for (const tagInput of tagInputs) {
|
|
23
|
+
if ("id" in tagInput && tagInput.id) {
|
|
24
|
+
tagsWithIds.push({
|
|
25
|
+
id: tagInput.id,
|
|
26
|
+
name: normalizeTagName(tagInput.name),
|
|
27
|
+
slug: tagInput.slug,
|
|
28
|
+
createdAt: new Date(),
|
|
29
|
+
updatedAt: new Date(),
|
|
30
|
+
} as Tag);
|
|
31
|
+
} else {
|
|
32
|
+
tagsToFindOrCreate.push({ name: normalizeTagName(tagInput.name) });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (tagsToFindOrCreate.length === 0) {
|
|
37
|
+
return tagsWithIds;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const allTags = await adapter.findMany<Tag>({ model: "tag" });
|
|
41
|
+
const tagMapBySlug = new Map<string, Tag>();
|
|
42
|
+
for (const tag of allTags) {
|
|
43
|
+
tagMapBySlug.set(tag.slug, tag);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const tagSlugs = tagsToFindOrCreate.map((tag) => slugify(tag.name));
|
|
47
|
+
const foundTags: Tag[] = [];
|
|
48
|
+
for (const slug of tagSlugs) {
|
|
49
|
+
const tag = tagMapBySlug.get(slug);
|
|
50
|
+
if (tag) {
|
|
51
|
+
foundTags.push(tag);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const existingSlugs = new Set([
|
|
56
|
+
...tagsWithIds.map((tag) => tag.slug),
|
|
57
|
+
...foundTags.map((tag) => tag.slug),
|
|
58
|
+
]);
|
|
59
|
+
const tagsToCreate = tagsToFindOrCreate.filter(
|
|
60
|
+
(tag) => !existingSlugs.has(slugify(tag.name)),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const createdTags: Tag[] = [];
|
|
64
|
+
for (const tag of tagsToCreate) {
|
|
65
|
+
const normalizedName = normalizeTagName(tag.name);
|
|
66
|
+
const newTag = await adapter.create<Tag>({
|
|
67
|
+
model: "tag",
|
|
68
|
+
data: {
|
|
69
|
+
name: normalizedName,
|
|
70
|
+
slug: slugify(normalizedName),
|
|
71
|
+
createdAt: new Date(),
|
|
72
|
+
updatedAt: new Date(),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
createdTags.push(newTag);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [...tagsWithIds, ...foundTags, ...createdTags];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Input for creating a new blog post.
|
|
83
|
+
* `slug` must already be slugified by the caller.
|
|
84
|
+
*/
|
|
85
|
+
export interface CreatePostInput {
|
|
86
|
+
title: string;
|
|
87
|
+
content: string;
|
|
88
|
+
excerpt: string;
|
|
89
|
+
/** Pre-slugified URL slug — use {@link slugify} before passing. */
|
|
90
|
+
slug: string;
|
|
91
|
+
image?: string;
|
|
92
|
+
published?: boolean;
|
|
93
|
+
publishedAt?: Date;
|
|
94
|
+
createdAt?: Date;
|
|
95
|
+
updatedAt?: Date;
|
|
96
|
+
tags?: TagInput[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Input for updating an existing blog post.
|
|
101
|
+
* If `slug` is provided it must already be slugified by the caller.
|
|
102
|
+
*/
|
|
103
|
+
export interface UpdatePostInput {
|
|
104
|
+
title?: string;
|
|
105
|
+
content?: string;
|
|
106
|
+
excerpt?: string;
|
|
107
|
+
/** Pre-slugified URL slug — use {@link slugify} before passing. */
|
|
108
|
+
slug?: string;
|
|
109
|
+
image?: string;
|
|
110
|
+
published?: boolean;
|
|
111
|
+
publishedAt?: Date;
|
|
112
|
+
createdAt?: Date;
|
|
113
|
+
updatedAt?: Date;
|
|
114
|
+
tags?: TagInput[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a new blog post with optional tag associations.
|
|
119
|
+
* Pure DB function — no hooks, no HTTP context. Safe for server-side and SSG use.
|
|
120
|
+
*
|
|
121
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeCreatePost`) are NOT
|
|
122
|
+
* called. The caller is responsible for any access-control checks before
|
|
123
|
+
* invoking this function.
|
|
124
|
+
*
|
|
125
|
+
* @param adapter - The database adapter
|
|
126
|
+
* @param input - Post data; `slug` must be pre-slugified
|
|
127
|
+
*/
|
|
128
|
+
export async function createPost(
|
|
129
|
+
adapter: Adapter,
|
|
130
|
+
input: CreatePostInput,
|
|
131
|
+
): Promise<Post> {
|
|
132
|
+
const { tags: tagInputs, ...postData } = input;
|
|
133
|
+
const tagList = tagInputs ?? [];
|
|
134
|
+
|
|
135
|
+
const newPost = await adapter.create<Post>({
|
|
136
|
+
model: "post",
|
|
137
|
+
data: {
|
|
138
|
+
...postData,
|
|
139
|
+
published: postData.published ?? false,
|
|
140
|
+
tags: [] as Tag[],
|
|
141
|
+
createdAt: postData.createdAt ?? new Date(),
|
|
142
|
+
updatedAt: postData.updatedAt ?? new Date(),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (tagList.length > 0) {
|
|
147
|
+
const resolvedTags = await findOrCreateTags(adapter, tagList);
|
|
148
|
+
|
|
149
|
+
await adapter.transaction(async (tx) => {
|
|
150
|
+
for (const tag of resolvedTags) {
|
|
151
|
+
await tx.create<{ postId: string; tagId: string }>({
|
|
152
|
+
model: "postTag",
|
|
153
|
+
data: {
|
|
154
|
+
postId: newPost.id,
|
|
155
|
+
tagId: tag.id,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
newPost.tags = resolvedTags.map((tag) => ({ ...tag }));
|
|
162
|
+
} else {
|
|
163
|
+
newPost.tags = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return newPost;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Update an existing blog post and reconcile its tag associations.
|
|
171
|
+
* Returns `null` if no post with the given `id` exists.
|
|
172
|
+
* Pure DB function — no hooks, no HTTP context. Safe for server-side use.
|
|
173
|
+
*
|
|
174
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeUpdatePost`) are NOT
|
|
175
|
+
* called. The caller is responsible for any access-control checks before
|
|
176
|
+
* invoking this function.
|
|
177
|
+
*
|
|
178
|
+
* @param adapter - The database adapter
|
|
179
|
+
* @param id - The post ID to update
|
|
180
|
+
* @param input - Partial post data to apply; `slug` must be pre-slugified if provided
|
|
181
|
+
*/
|
|
182
|
+
export async function updatePost(
|
|
183
|
+
adapter: Adapter,
|
|
184
|
+
id: string,
|
|
185
|
+
input: UpdatePostInput,
|
|
186
|
+
): Promise<Post | null> {
|
|
187
|
+
const { tags: tagInputs, ...postData } = input;
|
|
188
|
+
|
|
189
|
+
return adapter.transaction(async (tx) => {
|
|
190
|
+
const updatedPost = await tx.update<Post>({
|
|
191
|
+
model: "post",
|
|
192
|
+
where: [{ field: "id", value: id }],
|
|
193
|
+
update: {
|
|
194
|
+
...postData,
|
|
195
|
+
updatedAt: new Date(),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!updatedPost) return null;
|
|
200
|
+
|
|
201
|
+
if (tagInputs !== undefined) {
|
|
202
|
+
const existingPostTags = await tx.findMany<{
|
|
203
|
+
postId: string;
|
|
204
|
+
tagId: string;
|
|
205
|
+
}>({
|
|
206
|
+
model: "postTag",
|
|
207
|
+
where: [{ field: "postId", value: id, operator: "eq" as const }],
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
for (const postTag of existingPostTags) {
|
|
211
|
+
await tx.delete<{ postId: string; tagId: string }>({
|
|
212
|
+
model: "postTag",
|
|
213
|
+
where: [
|
|
214
|
+
{
|
|
215
|
+
field: "postId",
|
|
216
|
+
value: postTag.postId,
|
|
217
|
+
operator: "eq" as const,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
field: "tagId",
|
|
221
|
+
value: postTag.tagId,
|
|
222
|
+
operator: "eq" as const,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (tagInputs.length > 0) {
|
|
229
|
+
const resolvedTags = await findOrCreateTags(adapter, tagInputs);
|
|
230
|
+
|
|
231
|
+
for (const tag of resolvedTags) {
|
|
232
|
+
await tx.create<{ postId: string; tagId: string }>({
|
|
233
|
+
model: "postTag",
|
|
234
|
+
data: {
|
|
235
|
+
postId: id,
|
|
236
|
+
tagId: tag.id,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
updatedPost.tags = resolvedTags.map((tag) => ({ ...tag }));
|
|
242
|
+
} else {
|
|
243
|
+
updatedPost.tags = [];
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
const existingPostTags = await tx.findMany<{
|
|
247
|
+
postId: string;
|
|
248
|
+
tagId: string;
|
|
249
|
+
}>({
|
|
250
|
+
model: "postTag",
|
|
251
|
+
where: [{ field: "postId", value: id, operator: "eq" as const }],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (existingPostTags.length > 0) {
|
|
255
|
+
const tagIds = existingPostTags.map((pt) => pt.tagId);
|
|
256
|
+
const tags = await tx.findMany<Tag>({
|
|
257
|
+
model: "tag",
|
|
258
|
+
});
|
|
259
|
+
updatedPost.tags = tags
|
|
260
|
+
.filter((tag) => tagIds.includes(tag.id))
|
|
261
|
+
.map((tag) => ({ ...tag }));
|
|
262
|
+
} else {
|
|
263
|
+
updatedPost.tags = [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return updatedPost;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Delete a blog post by ID.
|
|
273
|
+
* Pure DB function — no hooks, no HTTP context. Safe for server-side use.
|
|
274
|
+
*
|
|
275
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeDeletePost`) are NOT
|
|
276
|
+
* called. The caller is responsible for any access-control checks before
|
|
277
|
+
* invoking this function.
|
|
278
|
+
*
|
|
279
|
+
* @param adapter - The database adapter
|
|
280
|
+
* @param id - The post ID to delete
|
|
281
|
+
*/
|
|
282
|
+
export async function deletePost(adapter: Adapter, id: string): Promise<void> {
|
|
283
|
+
await adapter.delete<Post>({
|
|
284
|
+
model: "post",
|
|
285
|
+
where: [{ field: "id", value: id }],
|
|
286
|
+
});
|
|
287
|
+
}
|
|
@@ -7,6 +7,13 @@ 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 {
|
|
11
|
+
createPost as createPostMutation,
|
|
12
|
+
updatePost as updatePostMutation,
|
|
13
|
+
deletePost as deletePostMutation,
|
|
14
|
+
type CreatePostInput,
|
|
15
|
+
type UpdatePostInput,
|
|
16
|
+
} from "./mutations";
|
|
10
17
|
import { BLOG_QUERY_KEYS } from "./query-key-defs";
|
|
11
18
|
import { serializePost, serializeTag } from "./serializers";
|
|
12
19
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -260,85 +267,15 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
260
267
|
getPostBySlug: (slug: string) => getPostBySlug(adapter, slug),
|
|
261
268
|
getAllTags: () => getAllTags(adapter),
|
|
262
269
|
prefetchForRoute: createBlogPrefetchForRoute(adapter),
|
|
270
|
+
// Mutations
|
|
271
|
+
createPost: (input: CreatePostInput) =>
|
|
272
|
+
createPostMutation(adapter, input),
|
|
273
|
+
updatePost: (id: string, input: UpdatePostInput) =>
|
|
274
|
+
updatePostMutation(adapter, id, input),
|
|
275
|
+
deletePost: (id: string) => deletePostMutation(adapter, id),
|
|
263
276
|
}),
|
|
264
277
|
|
|
265
278
|
routes: (adapter: Adapter) => {
|
|
266
|
-
const findOrCreateTags = async (
|
|
267
|
-
tagInputs: Array<
|
|
268
|
-
{ name: string } | { id: string; name: string; slug: string }
|
|
269
|
-
>,
|
|
270
|
-
): Promise<Tag[]> => {
|
|
271
|
-
if (tagInputs.length === 0) return [];
|
|
272
|
-
|
|
273
|
-
const normalizeTagName = (name: string): string => {
|
|
274
|
-
return name.trim();
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const tagsWithIds: Tag[] = [];
|
|
278
|
-
const tagsToFindOrCreate: Array<{ name: string }> = [];
|
|
279
|
-
|
|
280
|
-
for (const tagInput of tagInputs) {
|
|
281
|
-
if ("id" in tagInput && tagInput.id) {
|
|
282
|
-
tagsWithIds.push({
|
|
283
|
-
id: tagInput.id,
|
|
284
|
-
name: normalizeTagName(tagInput.name),
|
|
285
|
-
slug: tagInput.slug,
|
|
286
|
-
createdAt: new Date(),
|
|
287
|
-
updatedAt: new Date(),
|
|
288
|
-
} as Tag);
|
|
289
|
-
} else {
|
|
290
|
-
tagsToFindOrCreate.push({ name: normalizeTagName(tagInput.name) });
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (tagsToFindOrCreate.length === 0) {
|
|
295
|
-
return tagsWithIds;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const allTags = await adapter.findMany<Tag>({
|
|
299
|
-
model: "tag",
|
|
300
|
-
});
|
|
301
|
-
const tagMapBySlug = new Map<string, Tag>();
|
|
302
|
-
for (const tag of allTags) {
|
|
303
|
-
tagMapBySlug.set(tag.slug, tag);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const tagSlugs = tagsToFindOrCreate.map((tag) => slugify(tag.name));
|
|
307
|
-
const foundTags: Tag[] = [];
|
|
308
|
-
|
|
309
|
-
for (const slug of tagSlugs) {
|
|
310
|
-
const tag = tagMapBySlug.get(slug);
|
|
311
|
-
if (tag) {
|
|
312
|
-
foundTags.push(tag);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const existingSlugs = new Set([
|
|
317
|
-
...tagsWithIds.map((tag) => tag.slug),
|
|
318
|
-
...foundTags.map((tag) => tag.slug),
|
|
319
|
-
]);
|
|
320
|
-
const tagsToCreate = tagsToFindOrCreate.filter(
|
|
321
|
-
(tag) => !existingSlugs.has(slugify(tag.name)),
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
const createdTags: Tag[] = [];
|
|
325
|
-
for (const tag of tagsToCreate) {
|
|
326
|
-
const normalizedName = normalizeTagName(tag.name);
|
|
327
|
-
const newTag = await adapter.create<Tag>({
|
|
328
|
-
model: "tag",
|
|
329
|
-
data: {
|
|
330
|
-
name: normalizedName,
|
|
331
|
-
slug: slugify(normalizedName),
|
|
332
|
-
createdAt: new Date(),
|
|
333
|
-
updatedAt: new Date(),
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
createdTags.push(newTag);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return [...tagsWithIds, ...foundTags, ...createdTags];
|
|
340
|
-
};
|
|
341
|
-
|
|
342
279
|
const listPosts = createEndpoint(
|
|
343
280
|
"/posts",
|
|
344
281
|
{
|
|
@@ -394,11 +331,17 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
394
331
|
);
|
|
395
332
|
}
|
|
396
333
|
|
|
397
|
-
|
|
398
|
-
const
|
|
334
|
+
// Destructure and discard createdAt/updatedAt — timestamps are always server-generated
|
|
335
|
+
const {
|
|
336
|
+
tags,
|
|
337
|
+
slug: rawSlug,
|
|
338
|
+
createdAt: _ca,
|
|
339
|
+
updatedAt: _ua,
|
|
340
|
+
...postData
|
|
341
|
+
} = ctx.body;
|
|
399
342
|
|
|
400
343
|
// Always slugify to ensure URL-safe slug, whether provided or generated from title
|
|
401
|
-
const slug = slugify(
|
|
344
|
+
const slug = slugify(rawSlug || postData.title);
|
|
402
345
|
|
|
403
346
|
// Validate that slugification produced a non-empty result
|
|
404
347
|
if (!slug) {
|
|
@@ -408,37 +351,14 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
408
351
|
});
|
|
409
352
|
}
|
|
410
353
|
|
|
411
|
-
const newPost = await adapter
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
createdAt: new Date(),
|
|
418
|
-
updatedAt: new Date(),
|
|
419
|
-
},
|
|
354
|
+
const newPost = await createPostMutation(adapter, {
|
|
355
|
+
...postData,
|
|
356
|
+
slug,
|
|
357
|
+
tags: tags ?? [],
|
|
358
|
+
createdAt: new Date(),
|
|
359
|
+
updatedAt: new Date(),
|
|
420
360
|
});
|
|
421
361
|
|
|
422
|
-
if (tagNames.length > 0) {
|
|
423
|
-
const createdTags = await findOrCreateTags(tagNames);
|
|
424
|
-
|
|
425
|
-
await adapter.transaction(async (tx) => {
|
|
426
|
-
for (const tag of createdTags) {
|
|
427
|
-
await tx.create<{ postId: string; tagId: string }>({
|
|
428
|
-
model: "postTag",
|
|
429
|
-
data: {
|
|
430
|
-
postId: newPost.id,
|
|
431
|
-
tagId: tag.id,
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
newPost.tags = createdTags.map((tag) => ({ ...tag }));
|
|
438
|
-
} else {
|
|
439
|
-
newPost.tags = [];
|
|
440
|
-
}
|
|
441
|
-
|
|
442
362
|
if (hooks?.onPostCreated) {
|
|
443
363
|
await hooks.onPostCreated(newPost, context);
|
|
444
364
|
}
|
|
@@ -475,8 +395,14 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
475
395
|
);
|
|
476
396
|
}
|
|
477
397
|
|
|
478
|
-
|
|
479
|
-
const
|
|
398
|
+
// Destructure and discard createdAt/updatedAt — timestamps are always server-generated
|
|
399
|
+
const {
|
|
400
|
+
tags,
|
|
401
|
+
slug: rawSlug,
|
|
402
|
+
createdAt: _ca,
|
|
403
|
+
updatedAt: _ua,
|
|
404
|
+
...restPostData
|
|
405
|
+
} = ctx.body;
|
|
480
406
|
|
|
481
407
|
// Sanitize slug if provided to ensure it's URL-safe
|
|
482
408
|
const slugified = rawSlug ? slugify(rawSlug) : undefined;
|
|
@@ -489,80 +415,16 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
489
415
|
});
|
|
490
416
|
}
|
|
491
417
|
|
|
492
|
-
const
|
|
418
|
+
const updated = await updatePostMutation(adapter, ctx.params.id, {
|
|
493
419
|
...restPostData,
|
|
494
420
|
...(slugified ? { slug: slugified } : {}),
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const updated = await adapter.transaction(async (tx) => {
|
|
498
|
-
const existingPostTags = await tx.findMany<{
|
|
499
|
-
postId: string;
|
|
500
|
-
tagId: string;
|
|
501
|
-
}>({
|
|
502
|
-
model: "postTag",
|
|
503
|
-
where: [
|
|
504
|
-
{
|
|
505
|
-
field: "postId",
|
|
506
|
-
value: ctx.params.id,
|
|
507
|
-
operator: "eq" as const,
|
|
508
|
-
},
|
|
509
|
-
],
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
const updatedPost = await tx.update<Post>({
|
|
513
|
-
model: "post",
|
|
514
|
-
where: [{ field: "id", value: ctx.params.id }],
|
|
515
|
-
update: {
|
|
516
|
-
...postData,
|
|
517
|
-
updatedAt: new Date(),
|
|
518
|
-
},
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
if (!updatedPost) {
|
|
522
|
-
throw ctx.error(404, {
|
|
523
|
-
message: "Post not found",
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
for (const postTag of existingPostTags) {
|
|
528
|
-
await tx.delete<{ postId: string; tagId: string }>({
|
|
529
|
-
model: "postTag",
|
|
530
|
-
where: [
|
|
531
|
-
{
|
|
532
|
-
field: "postId",
|
|
533
|
-
value: postTag.postId,
|
|
534
|
-
operator: "eq" as const,
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
field: "tagId",
|
|
538
|
-
value: postTag.tagId,
|
|
539
|
-
operator: "eq" as const,
|
|
540
|
-
},
|
|
541
|
-
],
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (tagNames.length > 0) {
|
|
546
|
-
const createdTags = await findOrCreateTags(tagNames);
|
|
547
|
-
|
|
548
|
-
for (const tag of createdTags) {
|
|
549
|
-
await tx.create<{ postId: string; tagId: string }>({
|
|
550
|
-
model: "postTag",
|
|
551
|
-
data: {
|
|
552
|
-
postId: ctx.params.id,
|
|
553
|
-
tagId: tag.id,
|
|
554
|
-
},
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
updatedPost.tags = createdTags.map((tag) => ({ ...tag }));
|
|
559
|
-
} else {
|
|
560
|
-
updatedPost.tags = [];
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return updatedPost;
|
|
421
|
+
tags: tags ?? [],
|
|
564
422
|
});
|
|
565
423
|
|
|
424
|
+
if (!updated) {
|
|
425
|
+
throw ctx.error(404, { message: "Post not found" });
|
|
426
|
+
}
|
|
427
|
+
|
|
566
428
|
if (hooks?.onPostUpdated) {
|
|
567
429
|
await hooks.onPostUpdated(updated, context);
|
|
568
430
|
}
|
|
@@ -597,10 +459,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
597
459
|
);
|
|
598
460
|
}
|
|
599
461
|
|
|
600
|
-
await adapter.
|
|
601
|
-
model: "post",
|
|
602
|
-
where: [{ field: "id", value: ctx.params.id }],
|
|
603
|
-
});
|
|
462
|
+
await deletePostMutation(adapter, ctx.params.id);
|
|
604
463
|
|
|
605
464
|
// Lifecycle hook
|
|
606
465
|
if (hooks?.onPostDeleted) {
|
|
@@ -7,12 +7,12 @@ import { QueryClient } from '@tanstack/react-query';
|
|
|
7
7
|
|
|
8
8
|
declare const createBoardSchema: z.ZodObject<{
|
|
9
9
|
description: z.ZodOptional<z.ZodString>;
|
|
10
|
-
name: z.ZodString;
|
|
11
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
12
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
13
10
|
slug: z.ZodOptional<z.ZodString>;
|
|
14
11
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
15
12
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
13
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
14
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
15
|
+
name: z.ZodString;
|
|
16
16
|
}, z.core.$strip>;
|
|
17
17
|
declare const updateBoardSchema: z.ZodObject<{
|
|
18
18
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
|
|
|
28
28
|
title: z.ZodString;
|
|
29
29
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
30
30
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
31
|
-
boardId: z.ZodString;
|
|
32
31
|
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
32
|
+
boardId: z.ZodString;
|
|
33
33
|
}, z.core.$strip>;
|
|
34
34
|
declare const updateColumnSchema: z.ZodObject<{
|
|
35
35
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
@@ -331,19 +331,19 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
331
331
|
body: better_call.StandardSchemaV1<{
|
|
332
332
|
name: string;
|
|
333
333
|
description?: string | undefined;
|
|
334
|
-
createdAt?: unknown;
|
|
335
|
-
updatedAt?: unknown;
|
|
336
334
|
slug?: string | undefined;
|
|
337
335
|
ownerId?: string | undefined;
|
|
338
336
|
organizationId?: string | undefined;
|
|
337
|
+
createdAt?: unknown;
|
|
338
|
+
updatedAt?: unknown;
|
|
339
339
|
}, {
|
|
340
340
|
name: string;
|
|
341
341
|
description?: string | undefined;
|
|
342
|
-
createdAt?: unknown;
|
|
343
|
-
updatedAt?: unknown;
|
|
344
342
|
slug?: string | undefined;
|
|
345
343
|
ownerId?: string | undefined;
|
|
346
344
|
organizationId?: string | undefined;
|
|
345
|
+
createdAt?: unknown;
|
|
346
|
+
updatedAt?: unknown;
|
|
347
347
|
}>;
|
|
348
348
|
}, {
|
|
349
349
|
columns: ColumnWithTasks[];
|
|
@@ -360,20 +360,20 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
360
360
|
method: "PUT";
|
|
361
361
|
body: better_call.StandardSchemaV1<{
|
|
362
362
|
description?: string | undefined;
|
|
363
|
-
name?: string | undefined;
|
|
364
|
-
createdAt?: unknown;
|
|
365
|
-
updatedAt?: unknown;
|
|
366
363
|
slug?: string | undefined;
|
|
367
364
|
ownerId?: string | undefined;
|
|
368
365
|
organizationId?: string | undefined;
|
|
369
|
-
}, {
|
|
370
|
-
description?: string | undefined;
|
|
371
|
-
name?: string | undefined;
|
|
372
366
|
createdAt?: unknown;
|
|
373
367
|
updatedAt?: unknown;
|
|
368
|
+
name?: string | undefined;
|
|
369
|
+
}, {
|
|
370
|
+
description?: string | undefined;
|
|
374
371
|
slug?: string | undefined;
|
|
375
372
|
ownerId?: string | undefined;
|
|
376
373
|
organizationId?: string | undefined;
|
|
374
|
+
createdAt?: unknown;
|
|
375
|
+
updatedAt?: unknown;
|
|
376
|
+
name?: string | undefined;
|
|
377
377
|
}>;
|
|
378
378
|
}, Board>;
|
|
379
379
|
readonly deleteBoard: better_call.StrictEndpoint<"/boards/:id", {} & {
|
|
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
404
404
|
title?: string | undefined;
|
|
405
405
|
createdAt?: unknown;
|
|
406
406
|
updatedAt?: unknown;
|
|
407
|
-
boardId?: string | undefined;
|
|
408
407
|
order?: number | undefined;
|
|
408
|
+
boardId?: string | undefined;
|
|
409
409
|
}, {
|
|
410
410
|
title?: string | undefined;
|
|
411
411
|
createdAt?: unknown;
|
|
412
412
|
updatedAt?: unknown;
|
|
413
|
-
boardId?: string | undefined;
|
|
414
413
|
order?: number | undefined;
|
|
414
|
+
boardId?: string | undefined;
|
|
415
415
|
}>;
|
|
416
416
|
}, Column>;
|
|
417
417
|
readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {
|
|
@@ -35,6 +35,13 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
+
tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
39
|
+
name: z.ZodString;
|
|
40
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
41
|
+
id: z.ZodString;
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
slug: z.ZodString;
|
|
44
|
+
}, z.core.$strip>]>>>>;
|
|
38
45
|
slug: z.ZodOptional<z.ZodString>;
|
|
39
46
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
40
47
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
@@ -44,13 +51,6 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
44
51
|
content: z.ZodString;
|
|
45
52
|
excerpt: z.ZodString;
|
|
46
53
|
image: z.ZodOptional<z.ZodString>;
|
|
47
|
-
tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
48
|
-
name: z.ZodString;
|
|
49
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
50
|
-
id: z.ZodString;
|
|
51
|
-
name: z.ZodString;
|
|
52
|
-
slug: z.ZodString;
|
|
53
|
-
}, z.core.$strip>]>>>>;
|
|
54
54
|
}, z.core.$strip>;
|
|
55
55
|
declare const updatePostSchema: z.ZodObject<{
|
|
56
56
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|