@btst/stack 2.11.1 → 2.11.3
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 +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/api/index.mjs +1 -0
- package/dist/plugins/blog/query-keys.d.cts +1 -1
- package/dist/plugins/blog/query-keys.d.mts +1 -1
- package/dist/plugins/blog/query-keys.d.ts +1 -1
- 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.qta0-CPq.d.ts → stack.BQSy-NDc.d.ts} +85 -2
- package/dist/shared/{stack.DvMUCTTl.d.mts → stack.CLrGRIj0.d.mts} +85 -2
- package/dist/shared/{stack.DEW8EtFu.d.cts → stack.CntKf20s.d.cts} +85 -2
- package/package.json +4 -4
- 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.BvCR4-9H.d.ts → stack.CMbX8Q5C.d.ts} +13 -13
- package/dist/shared/{stack.CWxAl9K3.d.mts → stack.Dj04W2c3.d.mts} +13 -13
- package/dist/shared/{stack.BOokfhZD.d.cts → stack.eq5eg1yt.d.cts} +13 -13
|
@@ -67,6 +67,86 @@ declare function getPostBySlug(adapter: DBAdapter, slug: string): Promise<(Post
|
|
|
67
67
|
*/
|
|
68
68
|
declare function getAllTags(adapter: DBAdapter): Promise<Tag[]>;
|
|
69
69
|
|
|
70
|
+
type TagInput = {
|
|
71
|
+
name: string;
|
|
72
|
+
} | {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
slug: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Input for creating a new blog post.
|
|
79
|
+
* `slug` must already be slugified by the caller.
|
|
80
|
+
*/
|
|
81
|
+
interface CreatePostInput {
|
|
82
|
+
title: string;
|
|
83
|
+
content: string;
|
|
84
|
+
excerpt: string;
|
|
85
|
+
/** Pre-slugified URL slug — use {@link slugify} before passing. */
|
|
86
|
+
slug: string;
|
|
87
|
+
image?: string;
|
|
88
|
+
published?: boolean;
|
|
89
|
+
publishedAt?: Date;
|
|
90
|
+
createdAt?: Date;
|
|
91
|
+
updatedAt?: Date;
|
|
92
|
+
tags?: TagInput[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Input for updating an existing blog post.
|
|
96
|
+
* If `slug` is provided it must already be slugified by the caller.
|
|
97
|
+
*/
|
|
98
|
+
interface UpdatePostInput {
|
|
99
|
+
title?: string;
|
|
100
|
+
content?: string;
|
|
101
|
+
excerpt?: string;
|
|
102
|
+
/** Pre-slugified URL slug — use {@link slugify} before passing. */
|
|
103
|
+
slug?: string;
|
|
104
|
+
image?: string;
|
|
105
|
+
published?: boolean;
|
|
106
|
+
publishedAt?: Date;
|
|
107
|
+
createdAt?: Date;
|
|
108
|
+
updatedAt?: Date;
|
|
109
|
+
tags?: TagInput[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create a new blog post with optional tag associations.
|
|
113
|
+
* Pure DB function — no hooks, no HTTP context. Safe for server-side and SSG use.
|
|
114
|
+
*
|
|
115
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeCreatePost`) are NOT
|
|
116
|
+
* called. The caller is responsible for any access-control checks before
|
|
117
|
+
* invoking this function.
|
|
118
|
+
*
|
|
119
|
+
* @param adapter - The database adapter
|
|
120
|
+
* @param input - Post data; `slug` must be pre-slugified
|
|
121
|
+
*/
|
|
122
|
+
declare function createPost(adapter: DBAdapter, input: CreatePostInput): Promise<Post>;
|
|
123
|
+
/**
|
|
124
|
+
* Update an existing blog post and reconcile its tag associations.
|
|
125
|
+
* Returns `null` if no post with the given `id` exists.
|
|
126
|
+
* Pure DB function — no hooks, no HTTP context. Safe for server-side use.
|
|
127
|
+
*
|
|
128
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeUpdatePost`) are NOT
|
|
129
|
+
* called. The caller is responsible for any access-control checks before
|
|
130
|
+
* invoking this function.
|
|
131
|
+
*
|
|
132
|
+
* @param adapter - The database adapter
|
|
133
|
+
* @param id - The post ID to update
|
|
134
|
+
* @param input - Partial post data to apply; `slug` must be pre-slugified if provided
|
|
135
|
+
*/
|
|
136
|
+
declare function updatePost(adapter: DBAdapter, id: string, input: UpdatePostInput): Promise<Post | null>;
|
|
137
|
+
/**
|
|
138
|
+
* Delete a blog post by ID.
|
|
139
|
+
* Pure DB function — no hooks, no HTTP context. Safe for server-side use.
|
|
140
|
+
*
|
|
141
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeDeletePost`) are NOT
|
|
142
|
+
* called. The caller is responsible for any access-control checks before
|
|
143
|
+
* invoking this function.
|
|
144
|
+
*
|
|
145
|
+
* @param adapter - The database adapter
|
|
146
|
+
* @param id - The post ID to delete
|
|
147
|
+
*/
|
|
148
|
+
declare function deletePost(adapter: DBAdapter, id: string): Promise<void>;
|
|
149
|
+
|
|
70
150
|
/**
|
|
71
151
|
* Route keys for the blog plugin — matches the keys returned by
|
|
72
152
|
* `stackClient.router.getRoute(path).routeKey`.
|
|
@@ -366,6 +446,9 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
366
446
|
}) | null>;
|
|
367
447
|
getAllTags: () => Promise<Tag[]>;
|
|
368
448
|
prefetchForRoute: BlogPrefetchForRoute;
|
|
449
|
+
createPost: (input: CreatePostInput) => Promise<Post>;
|
|
450
|
+
updatePost: (id: string, input: UpdatePostInput) => Promise<Post | null>;
|
|
451
|
+
deletePost: (id: string) => Promise<void>;
|
|
369
452
|
}>;
|
|
370
453
|
type BlogApiRouter = ReturnType<ReturnType<typeof blogBackendPlugin>["routes"]>;
|
|
371
454
|
|
|
@@ -476,5 +559,5 @@ declare function createBlogQueryKeys(client: ReturnType<typeof createApiClient<B
|
|
|
476
559
|
};
|
|
477
560
|
};
|
|
478
561
|
|
|
479
|
-
export { BLOG_QUERY_KEYS as B, NextPreviousPostsQuerySchema as N, getPostBySlug as a, getAllTags as b,
|
|
480
|
-
export type { PostListParams as P, PostListResult as c, BlogRouteKey as
|
|
562
|
+
export { BLOG_QUERY_KEYS as B, NextPreviousPostsQuerySchema as N, getPostBySlug as a, getAllTags as b, createPost as d, deletePost as e, createBlogQueryKeys as f, getAllPosts as g, PostListQuerySchema as i, blogBackendPlugin as l, updatePost as u };
|
|
563
|
+
export type { CreatePostInput as C, PostListParams as P, UpdatePostInput as U, PostListResult as c, BlogRouteKey as h, BlogApiContext as j, BlogBackendHooks as k, BlogApiRouter as m };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.3",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -742,7 +742,7 @@
|
|
|
742
742
|
}
|
|
743
743
|
},
|
|
744
744
|
"dependencies": {
|
|
745
|
-
"@btst/db": "2.
|
|
745
|
+
"@btst/db": "2.2.1",
|
|
746
746
|
"@lukemorales/query-key-factory": "^1.3.4",
|
|
747
747
|
"@milkdown/crepe": "^7.17.1",
|
|
748
748
|
"@milkdown/kit": "^7.17.1",
|
|
@@ -763,7 +763,7 @@
|
|
|
763
763
|
"@tanstack/react-query": "^5.0.0",
|
|
764
764
|
"@vercel/blob": ">=0.14.0",
|
|
765
765
|
"ai": ">=5.0.0",
|
|
766
|
-
"better-call": ">=1.3.
|
|
766
|
+
"better-call": ">=1.3.5",
|
|
767
767
|
"class-variance-authority": ">=0.7.0",
|
|
768
768
|
"clsx": ">=2.1.0",
|
|
769
769
|
"cmdk": ">=1.1.0",
|
|
@@ -801,7 +801,7 @@
|
|
|
801
801
|
"@ai-sdk/react": "^2.0.94",
|
|
802
802
|
"@aws-sdk/client-s3": "^3.1011.0",
|
|
803
803
|
"@aws-sdk/s3-request-presigner": "^3.1011.0",
|
|
804
|
-
"@btst/adapter-memory": "2.
|
|
804
|
+
"@btst/adapter-memory": "2.2.1",
|
|
805
805
|
"@btst/yar": "1.2.0",
|
|
806
806
|
"@types/react": "^19.0.0",
|
|
807
807
|
"@types/slug": "^5.0.9",
|
|
@@ -6,6 +6,13 @@ export {
|
|
|
6
6
|
type PostListParams,
|
|
7
7
|
type PostListResult,
|
|
8
8
|
} from "./getters";
|
|
9
|
+
export {
|
|
10
|
+
createPost,
|
|
11
|
+
updatePost,
|
|
12
|
+
deletePost,
|
|
13
|
+
type CreatePostInput,
|
|
14
|
+
type UpdatePostInput,
|
|
15
|
+
} from "./mutations";
|
|
9
16
|
export { serializePost, serializeTag } from "./serializers";
|
|
10
17
|
export { BLOG_QUERY_KEYS } from "./query-key-defs";
|
|
11
18
|
export { createBlogQueryKeys } from "../query-keys";
|
|
@@ -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
|
+
}
|