@btst/stack 2.11.0 → 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/packages/stack/src/plugins/media/api/plugin.cjs +4 -1
- package/dist/packages/stack/src/plugins/media/api/plugin.mjs +5 -2
- 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/{packages/stack/src/plugins → plugins}/media/api/adapters/local.cjs +7 -1
- package/dist/plugins/media/api/adapters/local.d.cts +30 -0
- package/dist/plugins/media/api/adapters/local.d.mts +30 -0
- package/dist/plugins/media/api/adapters/local.d.ts +30 -0
- package/dist/{packages/stack/src/plugins → plugins}/media/api/adapters/local.mjs +7 -1
- package/dist/plugins/media/api/adapters/s3.d.cts +1 -1
- package/dist/plugins/media/api/adapters/s3.d.mts +1 -1
- package/dist/plugins/media/api/adapters/s3.d.ts +1 -1
- package/dist/plugins/media/api/adapters/vercel-blob.d.cts +1 -1
- package/dist/plugins/media/api/adapters/vercel-blob.d.mts +1 -1
- package/dist/plugins/media/api/adapters/vercel-blob.d.ts +1 -1
- package/dist/plugins/media/api/index.cjs +0 -2
- package/dist/plugins/media/api/index.d.cts +5 -102
- package/dist/plugins/media/api/index.d.mts +5 -102
- package/dist/plugins/media/api/index.d.ts +5 -102
- package/dist/plugins/media/api/index.mjs +0 -1
- package/dist/plugins/media/query-keys.d.cts +2 -2
- package/dist/plugins/media/query-keys.d.mts +2 -2
- package/dist/plugins/media/query-keys.d.ts +2 -2
- package/dist/shared/{stack.Bci0-plK.d.ts → stack.C5ucdatf.d.ts} +76 -3
- package/dist/shared/{stack.D7HSzZdG.d.ts → stack.D0p6oNme.d.ts} +90 -7
- package/dist/shared/{stack.6mEHS2WH.d.mts → stack.DOZ1EXjM.d.mts} +3 -3
- package/dist/shared/{stack.IUeyQKrm.d.mts → stack.DWipT53I.d.cts} +90 -7
- package/dist/shared/{stack.AJTXI7kw.d.cts → stack.DX-tQ93o.d.cts} +3 -3
- package/dist/shared/{stack.C_MUwwgR.d.mts → stack.DpZoZd98.d.mts} +76 -3
- package/dist/shared/{stack.DjgpFWq3.d.cts → stack.E17kSK1W.d.mts} +90 -7
- package/dist/shared/{stack.QYn-Px94.d.ts → stack.VF6FhyZw.d.ts} +3 -3
- package/dist/shared/{stack.DO6vOGQG.d.cts → stack.lkebw2nj.d.cts} +1 -1
- package/dist/shared/{stack.DO6vOGQG.d.mts → stack.lkebw2nj.d.mts} +1 -1
- package/dist/shared/{stack.DO6vOGQG.d.ts → stack.lkebw2nj.d.ts} +1 -1
- package/dist/shared/{stack.D6zyQnMo.d.cts → stack.vVFh38aS.d.cts} +76 -3
- package/package.json +14 -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/src/plugins/media/__tests__/storage-adapters.test.ts +11 -0
- package/src/plugins/media/api/adapters/local.ts +10 -1
- package/src/plugins/media/api/index.ts +0 -5
- package/src/plugins/media/api/plugin.ts +6 -0
- package/dist/shared/{stack.eq5eg1yt.d.cts → stack.B6S3cgwN.d.cts} +6 -6
- package/dist/shared/{stack.BQmuNl5p.d.ts → stack.BWp0hcm9.d.cts} +3 -3
- package/dist/shared/{stack.BQmuNl5p.d.cts → stack.BWp0hcm9.d.mts} +3 -3
- package/dist/shared/{stack.BQmuNl5p.d.mts → stack.BWp0hcm9.d.ts} +3 -3
- package/dist/shared/{stack.Dj04W2c3.d.mts → stack.Bzfx-_lq.d.mts} +6 -6
- package/dist/shared/{stack.CMbX8Q5C.d.ts → stack.j5SFLC1d.d.ts} +6 -6
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
|
|
2
|
-
import {
|
|
2
|
+
import { S as StorageAdapter, b as S3UploadToken } from './stack.lkebw2nj.cjs';
|
|
3
3
|
import { d as AssetListResult, l as listAssets, a as listFolders, A as AssetListParams } from './stack.jU2iG86n.cjs';
|
|
4
4
|
import * as better_call from 'better-call';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { A as Asset, F as Folder } from './stack.BMlLOMED.cjs';
|
|
7
|
+
import { DBAdapter } from '@btst/db';
|
|
7
8
|
|
|
8
9
|
declare const AssetListQuerySchema: z.ZodObject<{
|
|
9
10
|
folderId: z.ZodOptional<z.ZodString>;
|
|
@@ -21,6 +22,75 @@ declare const createFolderSchema: z.ZodObject<{
|
|
|
21
22
|
parentId: z.ZodOptional<z.ZodString>;
|
|
22
23
|
}, z.core.$strip>;
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Input for creating a new asset record.
|
|
27
|
+
*/
|
|
28
|
+
interface CreateAssetInput {
|
|
29
|
+
filename: string;
|
|
30
|
+
originalName: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
size: number;
|
|
33
|
+
url: string;
|
|
34
|
+
folderId?: string;
|
|
35
|
+
alt?: string;
|
|
36
|
+
tenantId?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Input for updating an existing asset record.
|
|
40
|
+
*/
|
|
41
|
+
interface UpdateAssetInput {
|
|
42
|
+
alt?: string;
|
|
43
|
+
folderId?: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Input for creating a new folder.
|
|
47
|
+
*/
|
|
48
|
+
interface CreateFolderInput {
|
|
49
|
+
name: string;
|
|
50
|
+
parentId?: string;
|
|
51
|
+
tenantId?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create an asset record in the database.
|
|
55
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
56
|
+
*
|
|
57
|
+
* @remarks **Security:** No authorization hooks (e.g. `onBeforeUpload`) are called.
|
|
58
|
+
* The caller is responsible for any access-control checks before invoking this function.
|
|
59
|
+
*/
|
|
60
|
+
declare function createAsset(adapter: DBAdapter, input: CreateAssetInput): Promise<Asset>;
|
|
61
|
+
/**
|
|
62
|
+
* Update an asset's `alt` text or `folderId`.
|
|
63
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
64
|
+
*
|
|
65
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
66
|
+
*/
|
|
67
|
+
declare function updateAsset(adapter: DBAdapter, id: string, input: UpdateAssetInput): Promise<Asset | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Delete an asset record from the database by its ID.
|
|
70
|
+
* Does NOT delete the underlying file — the caller must do that via the storage adapter.
|
|
71
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
72
|
+
*
|
|
73
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
74
|
+
*/
|
|
75
|
+
declare function deleteAsset(adapter: DBAdapter, id: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Create a folder record in the database.
|
|
78
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
79
|
+
*
|
|
80
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
81
|
+
*/
|
|
82
|
+
declare function createFolder(adapter: DBAdapter, input: CreateFolderInput): Promise<Folder>;
|
|
83
|
+
/**
|
|
84
|
+
* Delete a folder record from the database by its ID.
|
|
85
|
+
* Child folders are cascade-deleted automatically. Throws if the folder or
|
|
86
|
+
* any of its descendants contain assets (which have associated storage files
|
|
87
|
+
* that must be deleted via the storage adapter first).
|
|
88
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
89
|
+
*
|
|
90
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
91
|
+
*/
|
|
92
|
+
declare function deleteFolder(adapter: DBAdapter, id: string): Promise<void>;
|
|
93
|
+
|
|
24
94
|
/**
|
|
25
95
|
* Context passed to media API hooks.
|
|
26
96
|
*/
|
|
@@ -270,9 +340,12 @@ declare const mediaBackendPlugin: (config: MediaBackendConfig) => _btst_stack_pl
|
|
|
270
340
|
}, {
|
|
271
341
|
listAssets: (params?: Parameters<typeof listAssets>[1]) => Promise<AssetListResult>;
|
|
272
342
|
getAssetById: (id: string) => Promise<Asset | null>;
|
|
343
|
+
createAsset: (input: Parameters<typeof createAsset>[1]) => Promise<Asset>;
|
|
344
|
+
updateAsset: (id: string, input: Parameters<typeof updateAsset>[2]) => Promise<Asset | null>;
|
|
273
345
|
listFolders: (params?: Parameters<typeof listFolders>[1]) => Promise<Folder[]>;
|
|
274
346
|
getFolderById: (id: string) => Promise<Folder | null>;
|
|
275
347
|
getFolderByName: (name: string, parentId?: string | null, tenantId?: string) => Promise<Folder | null>;
|
|
348
|
+
createFolder: (input: Parameters<typeof createFolder>[1]) => Promise<Folder>;
|
|
276
349
|
}>;
|
|
277
350
|
type MediaApiRouter = ReturnType<ReturnType<typeof mediaBackendPlugin>["routes"]>;
|
|
278
351
|
|
|
@@ -300,5 +373,5 @@ declare const MEDIA_QUERY_KEYS: {
|
|
|
300
373
|
foldersList: (parentId?: string | null) => readonly ["media", "folders", "list", string];
|
|
301
374
|
};
|
|
302
375
|
|
|
303
|
-
export { MEDIA_QUERY_KEYS as a,
|
|
304
|
-
export type { AssetListDiscriminator as A,
|
|
376
|
+
export { MEDIA_QUERY_KEYS as M, createFolder as a, deleteFolder as b, createAsset as c, deleteAsset as d, assetListDiscriminator as f, mediaBackendPlugin as m, updateAsset as u };
|
|
377
|
+
export type { AssetListDiscriminator as A, CreateAssetInput as C, UpdateAssetInput as U, CreateFolderInput as e, MediaApiContext as g, MediaBackendHooks as h, MediaBackendConfig as i, MediaApiRouter as j };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.2",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -424,6 +424,16 @@
|
|
|
424
424
|
"default": "./dist/plugins/media/api/index.cjs"
|
|
425
425
|
}
|
|
426
426
|
},
|
|
427
|
+
"./plugins/media/api/adapters/local": {
|
|
428
|
+
"import": {
|
|
429
|
+
"types": "./dist/plugins/media/api/adapters/local.d.ts",
|
|
430
|
+
"default": "./dist/plugins/media/api/adapters/local.mjs"
|
|
431
|
+
},
|
|
432
|
+
"require": {
|
|
433
|
+
"types": "./dist/plugins/media/api/adapters/local.d.cts",
|
|
434
|
+
"default": "./dist/plugins/media/api/adapters/local.cjs"
|
|
435
|
+
}
|
|
436
|
+
},
|
|
427
437
|
"./plugins/media/api/adapters/s3": {
|
|
428
438
|
"import": {
|
|
429
439
|
"types": "./dist/plugins/media/api/adapters/s3.d.ts",
|
|
@@ -684,6 +694,9 @@
|
|
|
684
694
|
"plugins/media/api": [
|
|
685
695
|
"./dist/plugins/media/api/index.d.ts"
|
|
686
696
|
],
|
|
697
|
+
"plugins/media/api/adapters/local": [
|
|
698
|
+
"./dist/plugins/media/api/adapters/local.d.ts"
|
|
699
|
+
],
|
|
687
700
|
"plugins/media/api/adapters/s3": [
|
|
688
701
|
"./dist/plugins/media/api/adapters/s3.d.ts"
|
|
689
702
|
],
|
|
@@ -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
|
+
}
|