@btst/stack 1.2.2 → 1.3.1
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 +101 -120
- package/dist/packages/better-stack/src/plugins/blog/api/plugin.mjs +101 -120
- package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +1 -1
- package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +1 -1
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/on-this-page.cjs +1 -1
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/on-this-page.mjs +1 -1
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/recent-posts-carousel.cjs +2 -2
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/recent-posts-carousel.mjs +2 -2
- 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 +3 -3
- package/dist/plugins/blog/client/hooks/index.d.mts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.ts +3 -3
- 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 +6 -6
- package/dist/plugins/blog/query-keys.d.mts +6 -6
- package/dist/plugins/blog/query-keys.d.ts +6 -6
- package/package.json +3 -3
- package/src/plugins/blog/api/plugin.ts +122 -147
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +1 -1
- package/src/plugins/blog/client/components/shared/on-this-page.tsx +1 -1
- package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +2 -2
- package/src/plugins/blog/db.ts +10 -0
- package/src/plugins/blog/types.ts +7 -0
- package/dist/shared/{stack.Cr2JoQdo.d.cts → stack.CbuN2zVV.d.cts} +3 -3
- package/dist/shared/{stack.Cr2JoQdo.d.mts → stack.CbuN2zVV.d.mts} +3 -3
- package/dist/shared/{stack.Cr2JoQdo.d.ts → stack.CbuN2zVV.d.ts} +3 -3
|
@@ -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,49 @@ 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
|
+
}
|
|
348
|
+
|
|
349
|
+
// Fetch all tags at once
|
|
350
|
+
const tags =
|
|
351
|
+
tagIds.size > 0
|
|
352
|
+
? await adapter.findMany<Tag>({
|
|
353
|
+
model: "tag",
|
|
354
|
+
})
|
|
355
|
+
: [];
|
|
356
|
+
const tagMap = new Map<string, Tag>();
|
|
357
|
+
for (const tag of tags) {
|
|
358
|
+
if (tagIds.has(tag.id)) {
|
|
359
|
+
tagMap.set(tag.id, tag);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
413
362
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
363
|
+
// Map tags to posts (spread to avoid circular references)
|
|
364
|
+
let result = posts.map((post) => {
|
|
365
|
+
const postTags = (post.postTag || [])
|
|
366
|
+
.map((pt) => {
|
|
367
|
+
const tag = tagMap.get(pt.tagId);
|
|
368
|
+
return tag ? { ...tag } : undefined;
|
|
369
|
+
})
|
|
370
|
+
.filter((tag): tag is Tag => tag !== undefined);
|
|
371
|
+
const { postTag: _, ...postWithoutJoin } = post;
|
|
372
|
+
return {
|
|
373
|
+
...postWithoutJoin,
|
|
374
|
+
tags: postTags,
|
|
375
|
+
};
|
|
376
|
+
});
|
|
418
377
|
|
|
419
378
|
if (tagFilterPostIds) {
|
|
420
379
|
result = result.filter((post) => tagFilterPostIds!.has(post.id));
|
|
@@ -466,7 +425,6 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
466
425
|
body: ctx.body,
|
|
467
426
|
headers: ctx.headers,
|
|
468
427
|
};
|
|
469
|
-
const tagCache = createTagCache();
|
|
470
428
|
|
|
471
429
|
try {
|
|
472
430
|
if (hooks?.onBeforeCreatePost) {
|
|
@@ -496,7 +454,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
496
454
|
});
|
|
497
455
|
|
|
498
456
|
if (tagNames.length > 0) {
|
|
499
|
-
const createdTags = await findOrCreateTags(tagNames
|
|
457
|
+
const createdTags = await findOrCreateTags(tagNames);
|
|
500
458
|
|
|
501
459
|
await adapter.transaction(async (tx) => {
|
|
502
460
|
for (const tag of createdTags) {
|
|
@@ -540,7 +498,6 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
540
498
|
params: ctx.params,
|
|
541
499
|
headers: ctx.headers,
|
|
542
500
|
};
|
|
543
|
-
const tagCache = createTagCache();
|
|
544
501
|
|
|
545
502
|
try {
|
|
546
503
|
if (hooks?.onBeforeUpdatePost) {
|
|
@@ -608,7 +565,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
608
565
|
}
|
|
609
566
|
|
|
610
567
|
if (tagNames.length > 0) {
|
|
611
|
-
const createdTags = await findOrCreateTags(tagNames
|
|
568
|
+
const createdTags = await findOrCreateTags(tagNames);
|
|
612
569
|
|
|
613
570
|
for (const tag of createdTags) {
|
|
614
571
|
await tx.create<{ postId: string; tagId: string }>({
|
|
@@ -666,16 +623,9 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
666
623
|
}
|
|
667
624
|
}
|
|
668
625
|
|
|
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
|
-
});
|
|
626
|
+
await adapter.delete<Post>({
|
|
627
|
+
model: "post",
|
|
628
|
+
where: [{ field: "id", value: ctx.params.id }],
|
|
679
629
|
});
|
|
680
630
|
|
|
681
631
|
// Lifecycle hook
|
|
@@ -703,8 +653,6 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
703
653
|
async (ctx) => {
|
|
704
654
|
const { query, headers } = ctx;
|
|
705
655
|
const context: BlogApiContext = { query, headers };
|
|
706
|
-
const tagCache = createTagCache();
|
|
707
|
-
const postTagCache = createPostTagCache();
|
|
708
656
|
|
|
709
657
|
try {
|
|
710
658
|
if (hooks?.onBeforeListPosts) {
|
|
@@ -722,7 +670,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
722
670
|
const date = query.date;
|
|
723
671
|
|
|
724
672
|
// Get previous post (createdAt < date, newest first)
|
|
725
|
-
const
|
|
673
|
+
const previousPosts = await adapter.findMany<PostWithPostTag>({
|
|
726
674
|
model: "post",
|
|
727
675
|
limit: 1,
|
|
728
676
|
where: [
|
|
@@ -741,9 +689,12 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
741
689
|
field: "createdAt",
|
|
742
690
|
direction: "desc",
|
|
743
691
|
},
|
|
692
|
+
join: {
|
|
693
|
+
postTag: true,
|
|
694
|
+
},
|
|
744
695
|
});
|
|
745
696
|
|
|
746
|
-
const
|
|
697
|
+
const nextPosts = await adapter.findMany<PostWithPostTag>({
|
|
747
698
|
model: "post",
|
|
748
699
|
limit: 1,
|
|
749
700
|
where: [
|
|
@@ -762,31 +713,55 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
762
713
|
field: "createdAt",
|
|
763
714
|
direction: "asc",
|
|
764
715
|
},
|
|
716
|
+
join: {
|
|
717
|
+
postTag: true,
|
|
718
|
+
},
|
|
765
719
|
});
|
|
766
720
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
721
|
+
// Collect unique tag IDs from joined data
|
|
722
|
+
const tagIds = new Set<string>();
|
|
723
|
+
const allPosts = [...previousPosts, ...nextPosts];
|
|
724
|
+
for (const post of allPosts) {
|
|
725
|
+
if (post.postTag) {
|
|
726
|
+
for (const pt of post.postTag) {
|
|
727
|
+
tagIds.add(pt.tagId);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Fetch tags if needed
|
|
733
|
+
const tagMap = new Map<string, Tag>();
|
|
734
|
+
if (tagIds.size > 0) {
|
|
735
|
+
const tags = await adapter.findMany<Tag>({
|
|
736
|
+
model: "tag",
|
|
737
|
+
});
|
|
738
|
+
for (const tag of tags) {
|
|
739
|
+
if (tagIds.has(tag.id)) {
|
|
740
|
+
tagMap.set(tag.id, tag);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Helper to map post with tags (spread to avoid circular references)
|
|
746
|
+
const mapPostWithTags = (post: PostWithPostTag) => {
|
|
747
|
+
const tags = (post.postTag || [])
|
|
748
|
+
.map((pt) => {
|
|
749
|
+
const tag = tagMap.get(pt.tagId);
|
|
750
|
+
return tag ? { ...tag } : undefined;
|
|
751
|
+
})
|
|
752
|
+
.filter((tag): tag is Tag => tag !== undefined);
|
|
753
|
+
const { postTag: _, ...postWithoutJoin } = post;
|
|
754
|
+
return {
|
|
755
|
+
...postWithoutJoin,
|
|
756
|
+
tags,
|
|
757
|
+
};
|
|
758
|
+
};
|
|
776
759
|
|
|
777
760
|
return {
|
|
778
|
-
previous:
|
|
779
|
-
?
|
|
780
|
-
...previousPost[0],
|
|
781
|
-
tags: postTagsMap.get(previousPost[0].id) || [],
|
|
782
|
-
}
|
|
783
|
-
: null,
|
|
784
|
-
next: nextPost?.[0]
|
|
785
|
-
? {
|
|
786
|
-
...nextPost[0],
|
|
787
|
-
tags: postTagsMap.get(nextPost[0].id) || [],
|
|
788
|
-
}
|
|
761
|
+
previous: previousPosts[0]
|
|
762
|
+
? mapPostWithTags(previousPosts[0])
|
|
789
763
|
: null,
|
|
764
|
+
next: nextPosts[0] ? mapPostWithTags(nextPosts[0]) : null,
|
|
790
765
|
};
|
|
791
766
|
} catch (error) {
|
|
792
767
|
// Error hook
|
|
@@ -71,7 +71,7 @@ export function PostPage({ slug }: { slug: string }) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
return (
|
|
74
|
-
<PageWrapper className="gap-0 px-4 lg:px-
|
|
74
|
+
<PageWrapper className="gap-0 px-4 lg:px-2 py-0 pb-18" testId="post-page">
|
|
75
75
|
<div className="flex items-start w-full">
|
|
76
76
|
<div className="w-44 shrink-0 hidden xl:flex mr-auto" />
|
|
77
77
|
<div className="flex flex-col items-center flex-1 mx-auto w-full max-w-4xl min-w-0">
|
|
@@ -63,7 +63,7 @@ export function OnThisPage({ markdown, className }: OnThisPageProps) {
|
|
|
63
63
|
aria-label="Table of contents"
|
|
64
64
|
>
|
|
65
65
|
<div className="overflow-y-auto px-2">
|
|
66
|
-
<div className="flex flex-col gap-1 p-
|
|
66
|
+
<div className="flex flex-col gap-1 p-2 pt-0 text-sm">
|
|
67
67
|
<p className="flex items-center gap-2 font-semibold text-muted-foreground sticky top-0 h-6 text-xs">
|
|
68
68
|
<TextAlignStart className="w-3 h-3" />{" "}
|
|
69
69
|
{localization.BLOG_POST_ON_THIS_PAGE}
|
|
@@ -68,8 +68,8 @@ export function RecentPostsCarousel({ posts, ref }: RecentPostsCarouselProps) {
|
|
|
68
68
|
</CarouselItem>
|
|
69
69
|
))}
|
|
70
70
|
</CarouselContent>
|
|
71
|
-
<CarouselPrevious className="-left-4
|
|
72
|
-
<CarouselNext className="-right-4
|
|
71
|
+
<CarouselPrevious className="-left-4 z-50 hover:cursor-pointer" />
|
|
72
|
+
<CarouselNext className="-right-4 z-50 hover:cursor-pointer" />
|
|
73
73
|
</Carousel>
|
|
74
74
|
</div>
|
|
75
75
|
</>
|
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[];
|
|
@@ -35,15 +35,15 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
-
title: z.ZodString;
|
|
39
38
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
|
-
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
39
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
40
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
41
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
|
+
title: z.ZodString;
|
|
44
43
|
content: z.ZodString;
|
|
45
44
|
excerpt: z.ZodString;
|
|
46
45
|
image: z.ZodOptional<z.ZodString>;
|
|
46
|
+
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
47
47
|
tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
48
48
|
name: z.ZodString;
|
|
49
49
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -35,15 +35,15 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
-
title: z.ZodString;
|
|
39
38
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
|
-
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
39
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
40
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
41
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
|
+
title: z.ZodString;
|
|
44
43
|
content: z.ZodString;
|
|
45
44
|
excerpt: z.ZodString;
|
|
46
45
|
image: z.ZodOptional<z.ZodString>;
|
|
46
|
+
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
47
47
|
tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
48
48
|
name: z.ZodString;
|
|
49
49
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -35,15 +35,15 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
-
title: z.ZodString;
|
|
39
38
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
|
-
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
39
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
40
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
41
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
|
+
title: z.ZodString;
|
|
44
43
|
content: z.ZodString;
|
|
45
44
|
excerpt: z.ZodString;
|
|
46
45
|
image: z.ZodOptional<z.ZodString>;
|
|
46
|
+
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
47
47
|
tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
48
48
|
name: z.ZodString;
|
|
49
49
|
}, z.core.$strip>, z.ZodObject<{
|