@btst/stack 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/better-stack/src/plugins/blog/api/plugin.cjs +117 -137
- package/dist/packages/better-stack/src/plugins/blog/api/plugin.mjs +117 -137
- package/dist/packages/better-stack/src/plugins/blog/client/plugin.cjs +10 -0
- package/dist/packages/better-stack/src/plugins/blog/client/plugin.mjs +10 -0
- package/dist/packages/better-stack/src/plugins/blog/db.cjs +12 -2
- package/dist/packages/better-stack/src/plugins/blog/db.mjs +12 -2
- 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/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 +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.d.cts +5 -5
- package/dist/plugins/blog/query-keys.d.mts +5 -5
- package/dist/plugins/blog/query-keys.d.ts +5 -5
- package/package.json +3 -3
- package/src/plugins/blog/api/plugin.ts +139 -190
- package/src/plugins/blog/client/plugin.tsx +12 -0
- package/src/plugins/blog/db.ts +10 -0
- package/src/plugins/blog/types.ts +7 -0
- package/dist/shared/{stack.Cr2JoQdo.d.cts → stack.DLhzx1-D.d.cts} +2 -2
- package/dist/shared/{stack.Cr2JoQdo.d.mts → stack.DLhzx1-D.d.mts} +2 -2
- package/dist/shared/{stack.Cr2JoQdo.d.ts → stack.DLhzx1-D.d.ts} +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
|
|
2
2
|
import * as better_call from 'better-call';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.
|
|
4
|
+
import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.DLhzx1-D.js';
|
|
5
5
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
6
6
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
7
7
|
|
|
@@ -199,8 +199,6 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
199
199
|
slug?: string | undefined;
|
|
200
200
|
published?: boolean | undefined;
|
|
201
201
|
createdAt?: unknown;
|
|
202
|
-
publishedAt?: unknown;
|
|
203
|
-
updatedAt?: unknown;
|
|
204
202
|
image?: string | undefined;
|
|
205
203
|
tags?: ({
|
|
206
204
|
name: string;
|
|
@@ -209,6 +207,8 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
209
207
|
name: string;
|
|
210
208
|
slug: string;
|
|
211
209
|
})[] | undefined;
|
|
210
|
+
publishedAt?: unknown;
|
|
211
|
+
updatedAt?: unknown;
|
|
212
212
|
};
|
|
213
213
|
} & {
|
|
214
214
|
method?: "POST" | undefined;
|
|
@@ -239,8 +239,6 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
239
239
|
slug: z.ZodOptional<z.ZodString>;
|
|
240
240
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
241
241
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
242
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
243
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
244
242
|
content: z.ZodString;
|
|
245
243
|
excerpt: z.ZodString;
|
|
246
244
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -251,6 +249,8 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
251
249
|
name: z.ZodString;
|
|
252
250
|
slug: z.ZodString;
|
|
253
251
|
}, z.core.$strip>]>>>>;
|
|
252
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
253
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
254
254
|
}, z.core.$strip>;
|
|
255
255
|
};
|
|
256
256
|
path: "/posts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
}
|
|
158
158
|
},
|
|
159
159
|
"dependencies": {
|
|
160
|
-
"@btst/db": "2.0.
|
|
160
|
+
"@btst/db": "2.0.2",
|
|
161
161
|
"@lukemorales/query-key-factory": "^1.3.4",
|
|
162
162
|
"@milkdown/crepe": "^7.17.1",
|
|
163
163
|
"@milkdown/kit": "^7.17.1",
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
"zod": ">=3.24.0"
|
|
197
197
|
},
|
|
198
198
|
"devDependencies": {
|
|
199
|
-
"@btst/adapter-memory": "2.0.
|
|
199
|
+
"@btst/adapter-memory": "2.0.2",
|
|
200
200
|
"@btst/yar": "1.1.1",
|
|
201
201
|
"@types/react": "^19.0.0",
|
|
202
202
|
"@types/slug": "^5.0.9",
|
|
@@ -3,7 +3,7 @@ import { defineBackendPlugin } from "@btst/stack/plugins/api";
|
|
|
3
3
|
import { createEndpoint } from "@btst/stack/plugins/api";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { blogSchema as dbSchema } from "../db";
|
|
6
|
-
import type { Post, Tag } from "../types";
|
|
6
|
+
import type { Post, PostWithPostTag, Tag } from "../types";
|
|
7
7
|
import { slugify } from "../utils";
|
|
8
8
|
import { createPostSchema, updatePostSchema } from "../schemas";
|
|
9
9
|
|
|
@@ -169,68 +169,10 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
169
169
|
dbPlugin: dbSchema,
|
|
170
170
|
|
|
171
171
|
routes: (adapter: Adapter) => {
|
|
172
|
-
const createTagCache = () => {
|
|
173
|
-
let cache: Tag[] | null = null;
|
|
174
|
-
return {
|
|
175
|
-
getAllTags: async (): Promise<Tag[]> => {
|
|
176
|
-
if (!cache) {
|
|
177
|
-
cache = await adapter.findMany<Tag>({
|
|
178
|
-
model: "tag",
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
return cache;
|
|
182
|
-
},
|
|
183
|
-
invalidate: () => {
|
|
184
|
-
cache = null;
|
|
185
|
-
},
|
|
186
|
-
addTag: (tag: Tag) => {
|
|
187
|
-
if (cache) {
|
|
188
|
-
cache.push(tag);
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const createPostTagCache = () => {
|
|
195
|
-
let cache: Array<{ postId: string; tagId: string }> | null = null;
|
|
196
|
-
const getAllPostTags = async (): Promise<
|
|
197
|
-
Array<{ postId: string; tagId: string }>
|
|
198
|
-
> => {
|
|
199
|
-
if (!cache) {
|
|
200
|
-
cache = await adapter.findMany<{
|
|
201
|
-
postId: string;
|
|
202
|
-
tagId: string;
|
|
203
|
-
}>({
|
|
204
|
-
model: "postTag",
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
return cache;
|
|
208
|
-
};
|
|
209
|
-
return {
|
|
210
|
-
getAllPostTags,
|
|
211
|
-
invalidate: () => {
|
|
212
|
-
cache = null;
|
|
213
|
-
},
|
|
214
|
-
getByTagId: async (
|
|
215
|
-
tagId: string,
|
|
216
|
-
): Promise<Array<{ postId: string; tagId: string }>> => {
|
|
217
|
-
const allPostTags = await getAllPostTags();
|
|
218
|
-
return allPostTags.filter((pt) => pt.tagId === tagId);
|
|
219
|
-
},
|
|
220
|
-
getByPostId: async (
|
|
221
|
-
postId: string,
|
|
222
|
-
): Promise<Array<{ postId: string; tagId: string }>> => {
|
|
223
|
-
const allPostTags = await getAllPostTags();
|
|
224
|
-
return allPostTags.filter((pt) => pt.postId === postId);
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
};
|
|
228
|
-
|
|
229
172
|
const findOrCreateTags = async (
|
|
230
173
|
tagInputs: Array<
|
|
231
174
|
{ name: string } | { id: string; name: string; slug: string }
|
|
232
175
|
>,
|
|
233
|
-
tagCache: ReturnType<typeof createTagCache>,
|
|
234
176
|
): Promise<Tag[]> => {
|
|
235
177
|
if (tagInputs.length === 0) return [];
|
|
236
178
|
|
|
@@ -259,7 +201,9 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
259
201
|
return tagsWithIds;
|
|
260
202
|
}
|
|
261
203
|
|
|
262
|
-
const allTags = await
|
|
204
|
+
const allTags = await adapter.findMany<Tag>({
|
|
205
|
+
model: "tag",
|
|
206
|
+
});
|
|
263
207
|
const tagMapBySlug = new Map<string, Tag>();
|
|
264
208
|
for (const tag of allTags) {
|
|
265
209
|
tagMapBySlug.set(tag.slug, tag);
|
|
@@ -296,45 +240,11 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
296
240
|
},
|
|
297
241
|
});
|
|
298
242
|
createdTags.push(newTag);
|
|
299
|
-
tagCache.addTag(newTag);
|
|
300
243
|
}
|
|
301
244
|
|
|
302
245
|
return [...tagsWithIds, ...foundTags, ...createdTags];
|
|
303
246
|
};
|
|
304
247
|
|
|
305
|
-
const loadTagsForPosts = async (
|
|
306
|
-
postIds: string[],
|
|
307
|
-
tagCache: ReturnType<typeof createTagCache>,
|
|
308
|
-
postTagCache: ReturnType<typeof createPostTagCache>,
|
|
309
|
-
): Promise<Map<string, Tag[]>> => {
|
|
310
|
-
if (postIds.length === 0) return new Map();
|
|
311
|
-
|
|
312
|
-
const allPostTags = await postTagCache.getAllPostTags();
|
|
313
|
-
const relevantPostTags = allPostTags.filter((pt) =>
|
|
314
|
-
postIds.includes(pt.postId),
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
const tagIds = [...new Set(relevantPostTags.map((pt) => pt.tagId))];
|
|
318
|
-
if (tagIds.length === 0) return new Map();
|
|
319
|
-
|
|
320
|
-
const allTags = await tagCache.getAllTags();
|
|
321
|
-
const tagMap = new Map<string, Tag>();
|
|
322
|
-
for (const tag of allTags) {
|
|
323
|
-
tagMap.set(tag.id, tag);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const postTagsMap = new Map<string, Tag[]>();
|
|
327
|
-
for (const postTag of relevantPostTags) {
|
|
328
|
-
const tag = tagMap.get(postTag.tagId);
|
|
329
|
-
if (tag) {
|
|
330
|
-
const existing = postTagsMap.get(postTag.postId) || [];
|
|
331
|
-
postTagsMap.set(postTag.postId, [...existing, { ...tag }]);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return postTagsMap;
|
|
336
|
-
};
|
|
337
|
-
|
|
338
248
|
const listPosts = createEndpoint(
|
|
339
249
|
"/posts",
|
|
340
250
|
{
|
|
@@ -344,8 +254,6 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
344
254
|
async (ctx) => {
|
|
345
255
|
const { query, headers } = ctx;
|
|
346
256
|
const context: BlogApiContext = { query, headers };
|
|
347
|
-
const tagCache = createTagCache();
|
|
348
|
-
const postTagCache = createPostTagCache();
|
|
349
257
|
|
|
350
258
|
try {
|
|
351
259
|
if (hooks?.onBeforeListPosts) {
|
|
@@ -360,14 +268,34 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
360
268
|
let tagFilterPostIds: Set<string> | null = null;
|
|
361
269
|
|
|
362
270
|
if (query.tagSlug) {
|
|
363
|
-
const
|
|
364
|
-
|
|
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
|
+
});
|
|
365
281
|
|
|
366
282
|
if (!tag) {
|
|
367
283
|
return [];
|
|
368
284
|
}
|
|
369
285
|
|
|
370
|
-
const postTags = await
|
|
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
|
+
});
|
|
371
299
|
tagFilterPostIds = new Set(postTags.map((pt) => pt.postId));
|
|
372
300
|
if (tagFilterPostIds.size === 0) {
|
|
373
301
|
return [];
|
|
@@ -392,7 +320,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
392
320
|
});
|
|
393
321
|
}
|
|
394
322
|
|
|
395
|
-
const posts = await adapter.findMany<
|
|
323
|
+
const posts = await adapter.findMany<PostWithPostTag>({
|
|
396
324
|
model: "post",
|
|
397
325
|
limit:
|
|
398
326
|
query.query || query.tagSlug ? undefined : (query.limit ?? 10),
|
|
@@ -403,18 +331,46 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
403
331
|
field: "createdAt",
|
|
404
332
|
direction: "desc",
|
|
405
333
|
},
|
|
334
|
+
join: {
|
|
335
|
+
postTag: true,
|
|
336
|
+
},
|
|
406
337
|
});
|
|
407
338
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
}
|
|
413
348
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
364
|
+
let result = posts.map((post) => {
|
|
365
|
+
const postTags = (post.postTag || [])
|
|
366
|
+
.map((pt) => tagMap.get(pt.tagId))
|
|
367
|
+
.filter((tag): tag is Tag => tag !== undefined);
|
|
368
|
+
const { postTag: _, ...postWithoutJoin } = post;
|
|
369
|
+
return {
|
|
370
|
+
...postWithoutJoin,
|
|
371
|
+
tags: postTags,
|
|
372
|
+
};
|
|
373
|
+
});
|
|
418
374
|
|
|
419
375
|
if (tagFilterPostIds) {
|
|
420
376
|
result = result.filter((post) => tagFilterPostIds!.has(post.id));
|
|
@@ -466,7 +422,6 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
466
422
|
body: ctx.body,
|
|
467
423
|
headers: ctx.headers,
|
|
468
424
|
};
|
|
469
|
-
const tagCache = createTagCache();
|
|
470
425
|
|
|
471
426
|
try {
|
|
472
427
|
if (hooks?.onBeforeCreatePost) {
|
|
@@ -496,7 +451,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
496
451
|
});
|
|
497
452
|
|
|
498
453
|
if (tagNames.length > 0) {
|
|
499
|
-
const createdTags = await findOrCreateTags(tagNames
|
|
454
|
+
const createdTags = await findOrCreateTags(tagNames);
|
|
500
455
|
|
|
501
456
|
await adapter.transaction(async (tx) => {
|
|
502
457
|
for (const tag of createdTags) {
|
|
@@ -540,7 +495,6 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
540
495
|
params: ctx.params,
|
|
541
496
|
headers: ctx.headers,
|
|
542
497
|
};
|
|
543
|
-
const tagCache = createTagCache();
|
|
544
498
|
|
|
545
499
|
try {
|
|
546
500
|
if (hooks?.onBeforeUpdatePost) {
|
|
@@ -608,7 +562,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
608
562
|
}
|
|
609
563
|
|
|
610
564
|
if (tagNames.length > 0) {
|
|
611
|
-
const createdTags = await findOrCreateTags(tagNames
|
|
565
|
+
const createdTags = await findOrCreateTags(tagNames);
|
|
612
566
|
|
|
613
567
|
for (const tag of createdTags) {
|
|
614
568
|
await tx.create<{ postId: string; tagId: string }>({
|
|
@@ -666,16 +620,9 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
666
620
|
}
|
|
667
621
|
}
|
|
668
622
|
|
|
669
|
-
await adapter.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
where: [{ field: "postId", value: ctx.params.id }],
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
await tx.delete<Post>({
|
|
676
|
-
model: "post",
|
|
677
|
-
where: [{ field: "id", value: ctx.params.id }],
|
|
678
|
-
});
|
|
623
|
+
await adapter.delete<Post>({
|
|
624
|
+
model: "post",
|
|
625
|
+
where: [{ field: "id", value: ctx.params.id }],
|
|
679
626
|
});
|
|
680
627
|
|
|
681
628
|
// Lifecycle hook
|
|
@@ -703,8 +650,6 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
703
650
|
async (ctx) => {
|
|
704
651
|
const { query, headers } = ctx;
|
|
705
652
|
const context: BlogApiContext = { query, headers };
|
|
706
|
-
const tagCache = createTagCache();
|
|
707
|
-
const postTagCache = createPostTagCache();
|
|
708
653
|
|
|
709
654
|
try {
|
|
710
655
|
if (hooks?.onBeforeListPosts) {
|
|
@@ -720,27 +665,17 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
720
665
|
}
|
|
721
666
|
|
|
722
667
|
const date = query.date;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
// This avoids relying on comparison operators (lt/gt) which may not be
|
|
727
|
-
// consistently implemented across all database adapters (e.g., Drizzle).
|
|
728
|
-
//
|
|
729
|
-
// Strategy:
|
|
730
|
-
// 1. Fetch a window of recent published posts (sorted by date DESC)
|
|
731
|
-
// 2. Filter in memory to find posts immediately before/after target date
|
|
732
|
-
//
|
|
733
|
-
// Trade-offs:
|
|
734
|
-
// - Works reliably across all adapters (only uses eq operator and sorting)
|
|
735
|
-
// - Efficient for typical blog sizes (100 most recent posts)
|
|
736
|
-
// - Limitation: If target post is outside the window, we may not find neighbors
|
|
737
|
-
// (acceptable for typical blog navigation where users browse recent content)
|
|
738
|
-
const WINDOW_SIZE = 100;
|
|
739
|
-
|
|
740
|
-
const allPosts = await adapter.findMany<Post>({
|
|
668
|
+
|
|
669
|
+
// Get previous post (createdAt < date, newest first)
|
|
670
|
+
const previousPosts = await adapter.findMany<PostWithPostTag>({
|
|
741
671
|
model: "post",
|
|
742
|
-
limit:
|
|
672
|
+
limit: 1,
|
|
743
673
|
where: [
|
|
674
|
+
{
|
|
675
|
+
field: "createdAt",
|
|
676
|
+
value: date,
|
|
677
|
+
operator: "lt" as const,
|
|
678
|
+
},
|
|
744
679
|
{
|
|
745
680
|
field: "published",
|
|
746
681
|
value: true,
|
|
@@ -751,62 +686,76 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
751
686
|
field: "createdAt",
|
|
752
687
|
direction: "desc",
|
|
753
688
|
},
|
|
689
|
+
join: {
|
|
690
|
+
postTag: true,
|
|
691
|
+
},
|
|
754
692
|
});
|
|
755
693
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
694
|
+
const nextPosts = await adapter.findMany<PostWithPostTag>({
|
|
695
|
+
model: "post",
|
|
696
|
+
limit: 1,
|
|
697
|
+
where: [
|
|
698
|
+
{
|
|
699
|
+
field: "createdAt",
|
|
700
|
+
value: date,
|
|
701
|
+
operator: "gt" as const,
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
field: "published",
|
|
705
|
+
value: true,
|
|
706
|
+
operator: "eq" as const,
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
sortBy: {
|
|
710
|
+
field: "createdAt",
|
|
711
|
+
direction: "asc",
|
|
712
|
+
},
|
|
713
|
+
join: {
|
|
714
|
+
postTag: true,
|
|
715
|
+
},
|
|
761
716
|
});
|
|
762
717
|
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
718
|
+
// Collect unique tag IDs from joined data
|
|
719
|
+
const tagIds = new Set<string>();
|
|
720
|
+
const allPosts = [...previousPosts, ...nextPosts];
|
|
721
|
+
for (const post of allPosts) {
|
|
722
|
+
if (post.postTag) {
|
|
723
|
+
for (const pt of post.postTag) {
|
|
724
|
+
tagIds.add(pt.tagId);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Fetch tags if needed
|
|
730
|
+
const tagMap = new Map<string, Tag>();
|
|
731
|
+
if (tagIds.size > 0) {
|
|
732
|
+
const tags = await adapter.findMany<Tag>({
|
|
733
|
+
model: "tag",
|
|
734
|
+
});
|
|
735
|
+
for (const tag of tags) {
|
|
736
|
+
if (tagIds.has(tag.id)) {
|
|
737
|
+
tagMap.set(tag.id, tag);
|
|
738
|
+
}
|
|
783
739
|
}
|
|
784
|
-
// Skip posts with exactly the same timestamp (the current post itself)
|
|
785
740
|
}
|
|
786
741
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
742
|
+
// Helper to map post with tags
|
|
743
|
+
const mapPostWithTags = (post: PostWithPostTag) => {
|
|
744
|
+
const tags = (post.postTag || [])
|
|
745
|
+
.map((pt) => tagMap.get(pt.tagId))
|
|
746
|
+
.filter((tag): tag is Tag => tag !== undefined);
|
|
747
|
+
const { postTag: _, ...postWithoutJoin } = post;
|
|
748
|
+
return {
|
|
749
|
+
...postWithoutJoin,
|
|
750
|
+
tags,
|
|
751
|
+
};
|
|
752
|
+
};
|
|
796
753
|
|
|
797
754
|
return {
|
|
798
|
-
previous:
|
|
799
|
-
?
|
|
800
|
-
...previousPost,
|
|
801
|
-
tags: postTagsMap.get(previousPost.id) || [],
|
|
802
|
-
}
|
|
803
|
-
: null,
|
|
804
|
-
next: nextPost
|
|
805
|
-
? {
|
|
806
|
-
...nextPost,
|
|
807
|
-
tags: postTagsMap.get(nextPost.id) || [],
|
|
808
|
-
}
|
|
755
|
+
previous: previousPosts[0]
|
|
756
|
+
? mapPostWithTags(previousPosts[0])
|
|
809
757
|
: null,
|
|
758
|
+
next: nextPosts[0] ? mapPostWithTags(nextPosts[0]) : null,
|
|
810
759
|
};
|
|
811
760
|
} catch (error) {
|
|
812
761
|
// Error hook
|
|
@@ -755,6 +755,12 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
|
|
|
755
755
|
offset += limit;
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
+
// Fetch all tags
|
|
759
|
+
const tagsRes = await client("/tags", {
|
|
760
|
+
method: "GET",
|
|
761
|
+
});
|
|
762
|
+
const tags = (tagsRes.data ?? []) as unknown as SerializedTag[];
|
|
763
|
+
|
|
758
764
|
const getLastModified = (p: SerializedPost): Date | undefined => {
|
|
759
765
|
const dates = [p.updatedAt, p.publishedAt, p.createdAt].filter(
|
|
760
766
|
Boolean,
|
|
@@ -784,6 +790,12 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
|
|
|
784
790
|
changeFrequency: "monthly" as const,
|
|
785
791
|
priority: 0.6,
|
|
786
792
|
})),
|
|
793
|
+
...tags.map((t) => ({
|
|
794
|
+
url: `${origin}/blog/tag/${t.slug}`,
|
|
795
|
+
lastModified: t.updatedAt ? new Date(t.updatedAt) : undefined,
|
|
796
|
+
changeFrequency: "weekly" as const,
|
|
797
|
+
priority: 0.5,
|
|
798
|
+
})),
|
|
787
799
|
];
|
|
788
800
|
|
|
789
801
|
return entries;
|
package/src/plugins/blog/db.ts
CHANGED
|
@@ -80,10 +80,20 @@ export const blogSchema = createDbPlugin("blog", {
|
|
|
80
80
|
postId: {
|
|
81
81
|
type: "string",
|
|
82
82
|
required: true,
|
|
83
|
+
references: {
|
|
84
|
+
model: "post",
|
|
85
|
+
field: "id",
|
|
86
|
+
onDelete: "cascade",
|
|
87
|
+
},
|
|
83
88
|
},
|
|
84
89
|
tagId: {
|
|
85
90
|
type: "string",
|
|
86
91
|
required: true,
|
|
92
|
+
references: {
|
|
93
|
+
model: "tag",
|
|
94
|
+
field: "id",
|
|
95
|
+
onDelete: "cascade",
|
|
96
|
+
},
|
|
87
97
|
},
|
|
88
98
|
},
|
|
89
99
|
},
|
|
@@ -23,6 +23,13 @@ export type Tag = {
|
|
|
23
23
|
updatedAt: Date;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Post with joined postTag relationships from the database
|
|
28
|
+
*/
|
|
29
|
+
export type PostWithPostTag = Post & {
|
|
30
|
+
postTag?: Array<{ postId: string; tagId: string }>;
|
|
31
|
+
};
|
|
32
|
+
|
|
26
33
|
export interface SerializedPost
|
|
27
34
|
extends Omit<Post, "createdAt" | "updatedAt" | "publishedAt" | "tags"> {
|
|
28
35
|
tags: SerializedTag[];
|
|
@@ -39,8 +39,6 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
39
39
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
40
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
41
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
44
42
|
content: z.ZodString;
|
|
45
43
|
excerpt: z.ZodString;
|
|
46
44
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -51,6 +49,8 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
51
49
|
name: z.ZodString;
|
|
52
50
|
slug: z.ZodString;
|
|
53
51
|
}, z.core.$strip>]>>>>;
|
|
52
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
53
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
54
54
|
}, z.core.$strip>;
|
|
55
55
|
declare const updatePostSchema: z.ZodObject<{
|
|
56
56
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
@@ -39,8 +39,6 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
39
39
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
40
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
41
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
44
42
|
content: z.ZodString;
|
|
45
43
|
excerpt: z.ZodString;
|
|
46
44
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -51,6 +49,8 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
51
49
|
name: z.ZodString;
|
|
52
50
|
slug: z.ZodString;
|
|
53
51
|
}, z.core.$strip>]>>>>;
|
|
52
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
53
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
54
54
|
}, z.core.$strip>;
|
|
55
55
|
declare const updatePostSchema: z.ZodObject<{
|
|
56
56
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
@@ -39,8 +39,6 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
39
39
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
40
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
41
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
44
42
|
content: z.ZodString;
|
|
45
43
|
excerpt: z.ZodString;
|
|
46
44
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -51,6 +49,8 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
51
49
|
name: z.ZodString;
|
|
52
50
|
slug: z.ZodString;
|
|
53
51
|
}, z.core.$strip>]>>>>;
|
|
52
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
53
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
54
54
|
}, z.core.$strip>;
|
|
55
55
|
declare const updatePostSchema: z.ZodObject<{
|
|
56
56
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|