|
1 | | -import { eq } from 'drizzle-orm'; |
| 1 | +import { and, desc, eq, gte, lt } from 'drizzle-orm'; |
2 | 2 | import { db } from 'hub:db'; |
3 | 3 | import { kv } from 'hub:kv'; |
4 | 4 | import { blogPosts } from '~/server/db/schema'; |
@@ -32,44 +32,74 @@ export default defineEventHandler(async (event) => { |
32 | 32 | }); |
33 | 33 | } |
34 | 34 |
|
35 | | - const cacheKey = `nuxtpress:blog_post:${slug}:${year}:${month}:${day}`; |
| 35 | + const year0 = Number(year); |
| 36 | + const month0 = Number(month); |
| 37 | + const day0 = Number(day); |
| 38 | + |
| 39 | + if ( |
| 40 | + !Number.isInteger(year0) || |
| 41 | + !Number.isInteger(month0) || |
| 42 | + !Number.isInteger(day0) || |
| 43 | + year0 < 2000 || |
| 44 | + year0 > new Date().getUTCFullYear() + 1 || |
| 45 | + month0 < 1 || |
| 46 | + month0 > 12 || |
| 47 | + day0 < 1 || |
| 48 | + day0 > 31 |
| 49 | + ) { |
| 50 | + throw createError({ |
| 51 | + statusCode: 400, |
| 52 | + statusMessage: 'Date is out of range' |
| 53 | + }); |
| 54 | + } |
| 55 | + |
| 56 | + const cacheKey = `nuxtpress:blog_post:v2:${slug}:${year0}:${month0}:${day0}`; |
36 | 57 |
|
37 | 58 | // Check cache first |
38 | 59 | const cached = await kv.get(cacheKey); |
39 | 60 | if (cached && typeof cached === 'string') { |
40 | | - return JSON.parse(cached); |
| 61 | + try { |
| 62 | + return JSON.parse(cached); |
| 63 | + } catch { |
| 64 | + await kv.del(cacheKey); |
| 65 | + } |
41 | 66 | } |
42 | 67 |
|
43 | | - // Get all posts with the same slug |
44 | | - const rows = await db.select().from(blogPosts).where(eq(blogPosts.slug, slug)); |
| 68 | + const startOfDayUtcMs = Date.UTC(year0, month0 - 1, day0, 0, 0, 0, 0); |
| 69 | + const endOfDayUtcMs = startOfDayUtcMs + 24 * 60 * 60 * 1000; |
45 | 70 |
|
46 | | - const posts = rows.map((row) => ({ |
47 | | - ...row, |
48 | | - created_at: new Date(row.createdAt), |
49 | | - updated_at: new Date(row.updatedAt), |
50 | | - thumbnail: row.thumbnail |
51 | | - ? Uint8Array.from(atob(row.thumbnail), (c) => c.charCodeAt(0)) |
52 | | - : undefined, |
53 | | - thumbnail_url: row.thumbnailUrl, |
54 | | - tags: row.tags ? row.tags.split(',').map((t: string) => t.trim()) : [] |
55 | | - })) as BlogPost[]; |
| 71 | + const rows = await db |
| 72 | + .select() |
| 73 | + .from(blogPosts) |
| 74 | + .where( |
| 75 | + and( |
| 76 | + eq(blogPosts.slug, slug), |
| 77 | + gte(blogPosts.createdAt, new Date(startOfDayUtcMs)), |
| 78 | + lt(blogPosts.createdAt, new Date(endOfDayUtcMs)) |
| 79 | + ) |
| 80 | + ) |
| 81 | + .orderBy(desc(blogPosts.createdAt)) |
| 82 | + .limit(1); |
56 | 83 |
|
57 | | - if (!posts || posts.length === 0) { |
| 84 | + const row = rows[0]; |
| 85 | + |
| 86 | + if (!row) { |
58 | 87 | throw createError({ |
59 | 88 | statusCode: 404, |
60 | 89 | statusMessage: 'Post not found' |
61 | 90 | }); |
62 | 91 | } |
63 | 92 |
|
64 | | - // Construct the target date from the URL params |
65 | | - const targetDate = new Date(Number(year), Number(month) - 1, Number(day)); |
66 | | - |
67 | | - // Find the post with the closest date to the target |
68 | | - const res = posts.reduce((closest, current) => { |
69 | | - const closestDiff = Math.abs(closest.created_at.getTime() - targetDate.getTime()); |
70 | | - const currentDiff = Math.abs(current.created_at.getTime() - targetDate.getTime()); |
71 | | - return currentDiff < closestDiff ? current : closest; |
72 | | - }); |
| 93 | + const res = { |
| 94 | + ...row, |
| 95 | + created_at: new Date(row.createdAt), |
| 96 | + updated_at: new Date(row.updatedAt), |
| 97 | + thumbnail: row.thumbnail |
| 98 | + ? Uint8Array.from(atob(row.thumbnail), (c) => c.charCodeAt(0)) |
| 99 | + : undefined, |
| 100 | + thumbnail_url: row.thumbnailUrl || undefined, |
| 101 | + tags: row.tags ? row.tags.split(',').map((t: string) => t.trim()) : [] |
| 102 | + } as BlogPost; |
73 | 103 |
|
74 | 104 | // Cache the result |
75 | 105 | await kv.set(cacheKey, JSON.stringify(res), { ttl: 60 * 60 * 4 }); // 4 hours |
|
0 commit comments