@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.
Files changed (26) hide show
  1. package/dist/packages/better-stack/src/plugins/blog/api/plugin.cjs +117 -137
  2. package/dist/packages/better-stack/src/plugins/blog/api/plugin.mjs +117 -137
  3. package/dist/packages/better-stack/src/plugins/blog/client/plugin.cjs +10 -0
  4. package/dist/packages/better-stack/src/plugins/blog/client/plugin.mjs +10 -0
  5. package/dist/packages/better-stack/src/plugins/blog/db.cjs +12 -2
  6. package/dist/packages/better-stack/src/plugins/blog/db.mjs +12 -2
  7. package/dist/plugins/blog/api/index.d.cts +1 -1
  8. package/dist/plugins/blog/api/index.d.mts +1 -1
  9. package/dist/plugins/blog/api/index.d.ts +1 -1
  10. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  11. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  12. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  13. package/dist/plugins/blog/client/index.d.cts +1 -1
  14. package/dist/plugins/blog/client/index.d.mts +1 -1
  15. package/dist/plugins/blog/client/index.d.ts +1 -1
  16. package/dist/plugins/blog/query-keys.d.cts +5 -5
  17. package/dist/plugins/blog/query-keys.d.mts +5 -5
  18. package/dist/plugins/blog/query-keys.d.ts +5 -5
  19. package/package.json +3 -3
  20. package/src/plugins/blog/api/plugin.ts +139 -190
  21. package/src/plugins/blog/client/plugin.tsx +12 -0
  22. package/src/plugins/blog/db.ts +10 -0
  23. package/src/plugins/blog/types.ts +7 -0
  24. package/dist/shared/{stack.Cr2JoQdo.d.cts → stack.DLhzx1-D.d.cts} +2 -2
  25. package/dist/shared/{stack.Cr2JoQdo.d.mts → stack.DLhzx1-D.d.mts} +2 -2
  26. 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.Cr2JoQdo.js';
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.2.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.1",
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.1",
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 tagCache.getAllTags();
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 allTags = await tagCache.getAllTags();
364
- const tag = allTags.find((t) => t.slug === query.tagSlug);
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 postTagCache.getByTagId(tag.id);
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<Post>({
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
- const postTagsMap = await loadTagsForPosts(
409
- posts.map((post) => post.id),
410
- tagCache,
411
- postTagCache,
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
- let result = posts.map((post) => ({
415
- ...post,
416
- tags: postTagsMap.get(post.id) || [],
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, tagCache);
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, tagCache);
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.transaction(async (tx) => {
670
- await tx.delete({
671
- model: "postTag",
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
- const targetTime = new Date(date).getTime();
724
-
725
- // Window-based approach for finding next/previous posts
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: WINDOW_SIZE,
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
- // Sort posts by createdAt descending (newest first)
757
- const sortedPosts = allPosts.sort((a, b) => {
758
- const timeA = new Date(a.createdAt).getTime();
759
- const timeB = new Date(b.createdAt).getTime();
760
- return timeB - timeA;
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
- // Find posts immediately before and after the target date
764
- // In DESC sorted array: newer posts come before, older posts come after
765
- let previousPost: Post | null = null;
766
- let nextPost: Post | null = null;
767
-
768
- for (let i = 0; i < sortedPosts.length; i++) {
769
- const post = sortedPosts[i];
770
- if (!post) continue;
771
-
772
- const postTime = new Date(post.createdAt).getTime();
773
-
774
- if (postTime > targetTime) {
775
- // This post is newer than target - it's a next post candidate
776
- // Keep the last one we find (closest to target date)
777
- nextPost = post;
778
- } else if (postTime < targetTime) {
779
- // This is the first post older than target - this is the previous post
780
- previousPost = post;
781
- // We've found both neighbors, no need to continue
782
- break;
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
- const postIds = [
788
- ...(previousPost ? [previousPost.id] : []),
789
- ...(nextPost ? [nextPost.id] : []),
790
- ];
791
- const postTagsMap = await loadTagsForPosts(
792
- postIds,
793
- tagCache,
794
- postTagCache,
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: previousPost
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;
@@ -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>>;